Skip to content

Commit

Permalink
feat: multi osc device
Browse files Browse the repository at this point in the history
  • Loading branch information
mint-dewit committed Dec 15, 2022
1 parent 14167b0 commit b987680
Show file tree
Hide file tree
Showing 10 changed files with 558 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ exports[`index imports 1`] = `
"TimelineContentTypeHTTPParamType",
"TimelineContentTypeHyperdeck",
"TimelineContentTypeLawo",
"TimelineContentTypeMultiOSC",
"TimelineContentTypeOBS",
"TimelineContentTypeOSC",
"TimelineContentTypePanasonicPtz",
Expand Down
4 changes: 4 additions & 0 deletions packages/timeline-state-resolver-types/src/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
HTTPWatcherOptions,
VizMSEOptions,
VMixOptions,
MultiOSCOptions,
} from '.'
import { ShotokuOptions } from './shotoku'
import { TelemetricsOptions } from './telemetrics'
Expand Down Expand Up @@ -137,3 +138,6 @@ export interface DeviceOptionsVMix extends DeviceOptionsBase<VMixOptions> {
export interface DeviceOptionsTelemetrics extends DeviceOptionsBase<TelemetricsOptions> {
type: DeviceType.TELEMETRICS
}
export interface DeviceOptionsMultiOSC extends DeviceOptionsBase<MultiOSCOptions> {
type: DeviceType.MULTI_OSC
}
2 changes: 2 additions & 0 deletions packages/timeline-state-resolver-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export * from './singularLive'
export * from './vmix'
export * from './obs'
export * from './telemetrics'
export * from './multiOsc'

export * from './device'
export * from './mapping'
Expand Down Expand Up @@ -74,6 +75,7 @@ export enum DeviceType {
OBS = 21,
SOFIE_CHEF = 22,
TELEMETRICS = 23,
MULTI_OSC = 24,
}

export type TSRTimelineKeyframe<TContent> = Timeline.TimelineKeyframe<TContent>
Expand Down
21 changes: 21 additions & 0 deletions packages/timeline-state-resolver-types/src/multiOsc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Mapping } from './mapping'
import { DeviceType, OSCDeviceType } from '.'

export interface MultiOSCOptions {
connections: {
connectionId: string
host: string
port: number
type: OSCDeviceType
}[]
timeBetweenCommands: number // todo - move this to the individual timeline objects?
}

export interface MappingMultiOSC extends Mapping {
device: DeviceType.MULTI_OSC
connectionId: string
}

export enum TimelineContentTypeMultiOSC {
OSC = 'osc',
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ exports[`index imports 1`] = `
"TimelineContentTypeHTTPParamType",
"TimelineContentTypeHyperdeck",
"TimelineContentTypeLawo",
"TimelineContentTypeMultiOSC",
"TimelineContentTypeOBS",
"TimelineContentTypeOSC",
"TimelineContentTypePanasonicPtz",
Expand Down
14 changes: 13 additions & 1 deletion packages/timeline-state-resolver/src/conductor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
Timeline,
TSRTimelineContent,
TimelineDatastoreReferencesContent,
DeviceOptionsMultiOSC,
} from 'timeline-state-resolver-types'

import { DoOnTime } from './devices/doOnTime'
Expand Down Expand Up @@ -48,6 +49,7 @@ import { VizMSEDevice, DeviceOptionsVizMSEInternal } from './integrations/vizMSE
import { ShotokuDevice, DeviceOptionsShotokuInternal } from './integrations/shotoku'
import { DeviceOptionsSofieChefInternal, SofieChefDevice } from './integrations/sofieChef'
import { TelemetricsDevice } from './integrations/telemetrics'
import { MultiOSCMessageDevice } from './integrations/multiOsc'

export { DeviceContainer }
export { CommandWithContext }
Expand Down Expand Up @@ -601,13 +603,22 @@ export class Conductor extends EventEmitter<ConductorEvents> {
)
} else if (deviceOptions.type === DeviceType.TELEMETRICS) {
newDevice = await DeviceContainer.create<DeviceOptionsTelemetrics, typeof TelemetricsDevice>(
'../../dist/devices/telemetrics.js',
'../../dist/integrations/telemetrics/index.js',
'TelemetricsDevice',
deviceId,
deviceOptions,
getCurrentTime,
threadedClassOptions
)
} else if (deviceOptions.type === DeviceType.MULTI_OSC) {
newDevice = await DeviceContainer.create<DeviceOptionsMultiOSC, typeof MultiOSCMessageDevice>(
'../../dist/integrations/multiOsc/index.js',
'MultiOSCMessageDevice',
deviceId,
deviceOptions,
getCurrentTime,
threadedClassOptions
)
} else {
// @ts-ignore deviceOptions.type is of type "never"
const type: any = deviceOptions.type
Expand Down Expand Up @@ -1503,6 +1514,7 @@ export type DeviceOptionsAnyInternal =
| DeviceOptionsShotokuInternal
| DeviceOptionsVizMSEInternal
| DeviceOptionsTelemetrics
| DeviceOptionsMultiOSC

function removeParentFromState(
o: Timeline.TimelineState<TSRTimelineContent>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { Conductor } from '../../../conductor'
import { OSCMessageDevice } from '../../osc'
import {
Mappings,
DeviceType,
TimelineContentTypeOSC,
OSCValueType,
TimelineContentOSCMessage,
OSCDeviceType,
TSRTimelineObj,
MappingMultiOSC,
} from 'timeline-state-resolver-types'
import { MockTime } from '../../../__tests__/mockTime'
import { literal } from '../../../devices/device'
import { ThreadedClass } from 'threadedclass'
import { getMockCall } from '../../../__tests__/lib'

// let nowActual = Date.now()
describe('MultiOSC-Message', () => {
const mockTime = new MockTime()
beforeEach(() => {
mockTime.init()
})
test('MultiOSC message', async () => {
const commandReceiver0: any = jest.fn(async () => {
return Promise.resolve()
})
const myLayerMapping0: MappingMultiOSC = {
device: DeviceType.MULTI_OSC,
deviceId: 'osc0',
connectionId: 'osc0',
}
const myLayerMapping: Mappings = {
myLayer0: myLayerMapping0,
}

const myConductor = new Conductor({
multiThreadedResolver: false,
getCurrentTime: mockTime.getCurrentTime,
})
myConductor.on('error', (e) => console.error(e))
await myConductor.init()
await myConductor.addDevice('osc0', {
type: DeviceType.MULTI_OSC,
options: {
connections: [
{
connectionId: 'osc0',
host: '127.0.0.1',
port: 80,
type: OSCDeviceType.UDP,
},
],
timeBetweenCommands: 160,
},
// @ts-ignore: @todo - why doesn't it let me do this?
oscSenders: { osc0: commandReceiver0 },
})
myConductor.setTimelineAndMappings([], myLayerMapping)
await mockTime.advanceTimeToTicks(10100)

const deviceContainer = myConductor.getDevice('osc0')
const device = deviceContainer!.device as ThreadedClass<OSCMessageDevice>

// Check that no commands has been scheduled:
expect(await device.queue).toHaveLength(0)

myConductor.setTimelineAndMappings([
literal<TSRTimelineObj<TimelineContentOSCMessage>>({
id: 'obj0',
enable: {
start: mockTime.now + 1000, // in 1 second
duration: 2000,
},
layer: 'myLayer0',
content: {
deviceType: DeviceType.OSC,
type: TimelineContentTypeOSC.OSC,

path: '/test-path',
values: [
{
type: OSCValueType.INT,
value: 123,
},
{
type: OSCValueType.FLOAT,
value: 123.45,
},
{
type: OSCValueType.STRING,
value: 'abc',
},
{
type: OSCValueType.BLOB,
value: new Uint8Array([1, 3, 5]),
},
],
},
}),
])

await mockTime.advanceTimeToTicks(10990)
expect(commandReceiver0).toHaveBeenCalledTimes(0)
await mockTime.advanceTimeToTicks(11100)

expect(commandReceiver0).toHaveBeenCalledTimes(1)
// console.log(commandReceiver0.mock.calls)
expect(getMockCall(commandReceiver0, 0, 0)).toMatchObject({
address: '/test-path',
args: [
{
type: OSCValueType.INT,
value: 123,
},
{
type: OSCValueType.FLOAT,
value: 123.45,
},
{
type: OSCValueType.STRING,
value: 'abc',
},
{
type: OSCValueType.BLOB,
value: new Uint8Array([1, 3, 5]),
},
],
})
await mockTime.advanceTimeToTicks(16000)
expect(commandReceiver0).toHaveBeenCalledTimes(1)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as osc from 'osc'
import { EventEmitter } from 'events'
import { MultiOSCOptions, OSCDeviceType } from 'timeline-state-resolver-types'

export type OSCConnectionOptions = MultiOSCOptions['connections'][any] & {
oscSender?: OSCSender
}

type OSCSender = (msg: osc.OscMessage, address?: string | undefined, port?: number | undefined) => void

export class OSCConnection extends EventEmitter {
connectionId: string
host: string
port: number

private _oscClient: osc.UDPPort | osc.TCPSocketPort
private _oscSender: OSCSender

private _connected = false

/**
* Connnects to the OSC server.
* @param host ip to connect to
* @param port port the osc server is hosted on
*/
async connect(options: OSCConnectionOptions): Promise<void> {
this.connectionId = options.connectionId
this.host = options.host
this.port = options.port
this._oscSender = options.oscSender || this._defaultOscSender.bind(this)

if (options.type === OSCDeviceType.UDP) {
this._oscClient = new osc.UDPPort({
localAddress: '0.0.0.0',
localPort: 0,
remoteAddress: this.host,
remotePort: this.port,
metadata: true,
})
} else {
this._oscClient = new osc.TCPSocketPort({
address: this.host,
port: this.port,
metadata: true,
})
;(this._oscClient as osc.TCPSocketPort).socket.on('close', () => this.updateIsConnected(false))
;(this._oscClient as osc.TCPSocketPort).socket.on('connect', () => this.updateIsConnected(true))
}
this._oscClient.on('error', (error: any) => this.emit('error', error))
// this._oscClient.on('message', (received: osc.OscMessage) => this.receiver(received))

return new Promise((resolve) => {
this._oscClient.on('ready', () => {
resolve()
})
this._oscClient.open()
})
}
dispose() {
this.updateIsConnected(false)
this._oscClient.close()
}

private _defaultOscSender(msg: osc.OscMessage, address?: string | undefined, port?: number | undefined): void {
this.emit('debug', 'sending ' + msg.address)
this._oscClient.send(msg, address, port)
}

sendOsc(msg: osc.OscMessage, address?: string | undefined, port?: number | undefined): void {
this._oscSender(msg, address, port)
}

disconnect() {
this._oscClient.close()
}

get connected(): boolean {
return this._connected
}

private updateIsConnected(connected: boolean) {
if (this._connected !== connected) {
this._connected = connected

if (connected) {
this.emit('connected')
} else {
this.emit('disconnected')
}
}
}
}
Loading

0 comments on commit b987680

Please sign in to comment.