Skip to content

Commit

Permalink
feat(Hyperdeck): add support for play and goto commands
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Van Camp committed Aug 14, 2022
1 parent bfea388 commit 50e9e15
Show file tree
Hide file tree
Showing 3 changed files with 334 additions and 3 deletions.
10 changes: 9 additions & 1 deletion packages/timeline-state-resolver-types/src/hyperdeck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,16 @@ export interface TimelineObjHyperdeckTransport extends TimelineObjHyperdeck {
deviceType: DeviceType.HYPERDECK
type: TimelineContentTypeHyperdeck.TRANSPORT

/** The status of the hyperdeck. To start a recording, set to TransportStatus.RECORD */
/** The status of the hyperdeck. To start a recording, set to TransportStatus.RECORD. To start playback, set to TransportStatus.PLAY. */
status: TransportStatus
/** How fast to play the currently-playing clip [-5000 - 5000]. Default 1x speed is 100. 0 is stopped. Negative values are rewind. Values above 100 are fast-forward. */
speed: number
/** Whether or not to loop the currently-playing clip */
loop: boolean
/** Wheter or not to stop playback when the currently-playing clip is finished */
singleClip: boolean
/** The numeric ID of the clip to play. null = no clip */
clipId: number | null
/** The filename to record to */
recordFilename?: string
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Conductor } from '../../conductor'
import { HyperdeckDevice } from '../hyperdeck'
import { RecordCommand, StopCommand } from 'hyperdeck-connection/dist/commands'
import { GoToCommand, PlayCommand, RecordCommand, StopCommand } from 'hyperdeck-connection/dist/commands'
import * as HyperdeckConnection from '../../__mocks__/hyperdeck-connection'
import {
Mappings,
Expand Down Expand Up @@ -92,6 +92,10 @@ describe('Hyperdeck', () => {
type: TimelineContentTypeHyperdeck.TRANSPORT,

status: TransportStatus.RECORD,
speed: 0,
loop: false,
singleClip: true,
clipId: null,
recordFilename: 'sofie_dev',
},
},
Expand All @@ -107,6 +111,10 @@ describe('Hyperdeck', () => {
type: TimelineContentTypeHyperdeck.TRANSPORT,

status: TransportStatus.PREVIEW,
speed: 0,
loop: false,
singleClip: true,
clipId: null,
},
},
])
Expand Down Expand Up @@ -139,4 +147,247 @@ describe('Hyperdeck', () => {
expect(commandReceiver0).toHaveBeenCalledTimes(2)
// no new commands should have been sent, becuse obj2 is the same as obj1
})

test('Hyperdeck: Play', async () => {
let device: ThreadedClass<HyperdeckDevice> | undefined = undefined

const commandReceiver0: any = jest.fn((...args: any[]) => {
// Just forward the command:

// @ts-ignore private function
return device._defaultCommandReceiver(...args)
})
const myChannelMapping: Mappings = {
hyperdeck0_transport: myChannelMapping0,
}

const myConductor = new Conductor({
multiThreadedResolver: false,
getCurrentTime: mockTime.getCurrentTime,
})
myConductor.setTimelineAndMappings([], myChannelMapping)

await myConductor.init()
await myConductor.addDevice('hyperdeck0', {
type: DeviceType.HYPERDECK,
options: {
host: '127.0.0.1',
port: 9993,
},
commandReceiver: commandReceiver0,
})
await mockTime.advanceTimeToTicks(10100)

expect(commandReceiver0).toHaveBeenCalledTimes(0)

const hyperdeckInstances = HyperdeckConnection.Hyperdeck.getMockInstances()
expect(hyperdeckInstances).toHaveLength(2)

const hyperdeckMock: HyperdeckConnection.Hyperdeck = hyperdeckInstances[1]

const hyperdeckMockCommand = jest.fn(async () => {
return Promise.resolve()
})
hyperdeckMock.setMockCommandReceiver(hyperdeckMockCommand)

const deviceContainer = myConductor.getDevice('hyperdeck0')
device = deviceContainer!.device as ThreadedClass<HyperdeckDevice>

// Check that no commands has been scheduled:
expect(await device.queue).toHaveLength(0)
myConductor.setTimelineAndMappings([
{
id: 'obj0',
enable: {
start: 9000,
duration: 2000, // 11000
},
layer: 'hyperdeck0_transport',
content: {
deviceType: DeviceType.HYPERDECK,
type: TimelineContentTypeHyperdeck.TRANSPORT,

status: TransportStatus.PLAY,
speed: 100,
loop: false,
singleClip: true,
clipId: null,
},
},
{
id: 'obj1',
enable: {
start: 10500,
duration: 2000,
},
layer: 'hyperdeck0_transport',
content: {
deviceType: DeviceType.HYPERDECK,
type: TimelineContentTypeHyperdeck.TRANSPORT,

status: TransportStatus.PREVIEW,
speed: 100,
loop: false,
singleClip: true,
clipId: null,
},
},
])

await mockTime.advanceTimeToTicks(10200)

expect(commandReceiver0).toHaveBeenCalledTimes(1)
expect(getMockCall(commandReceiver0, 0, 1)).toBeInstanceOf(PlayCommand)
expect(getMockCall(commandReceiver0, 0, 1)).toHaveProperty('speed', '100')
expect(getMockCall(commandReceiver0, 0, 1)).toHaveProperty('loop', false)
expect(getMockCall(commandReceiver0, 0, 1)).toHaveProperty('singleClip', true)
expect(getMockCall(commandReceiver0, 0, 2)).toBeTruthy() // context
// also test the actual command sent to hyperdeck:
expect(hyperdeckMockCommand).toHaveBeenCalledTimes(1)
expect(getMockCall(hyperdeckMockCommand, 0, 0)).toBeInstanceOf(PlayCommand)
expect(getMockCall(hyperdeckMockCommand, 0, 0)).toMatchObject({
speed: '100',
loop: false,
singleClip: true,
})

myConductor.setTimelineAndMappings(myConductor.timeline) // Same timeline
await mockTime.advanceTimeToTicks(10400)
expect(hyperdeckMockCommand).toHaveBeenCalledTimes(1) // nothing has changed, so it should not be called again

await mockTime.advanceTimeToTicks(12000)

expect(commandReceiver0).toHaveBeenCalledTimes(2)
expect(getMockCall(commandReceiver0, 1, 1)).toBeInstanceOf(StopCommand)
expect(getMockCall(commandReceiver0, 1, 2)).toBeTruthy() // context

await mockTime.advanceTimeToTicks(13000)
expect(commandReceiver0).toHaveBeenCalledTimes(2)
// no new commands should have been sent, becuse obj2 is the same as obj1
})

test('Hyperdeck: GoTo', async () => {
let device: ThreadedClass<HyperdeckDevice> | undefined = undefined

const commandReceiver0: any = jest.fn((...args: any[]) => {
// Just forward the command:

// @ts-ignore private function
return device._defaultCommandReceiver(...args)
})
const myChannelMapping: Mappings = {
hyperdeck0_transport: myChannelMapping0,
}

const myConductor = new Conductor({
multiThreadedResolver: false,
getCurrentTime: mockTime.getCurrentTime,
})
myConductor.setTimelineAndMappings([], myChannelMapping)

await myConductor.init()
await myConductor.addDevice('hyperdeck0', {
type: DeviceType.HYPERDECK,
options: {
host: '127.0.0.1',
port: 9993,
},
commandReceiver: commandReceiver0,
})
await mockTime.advanceTimeToTicks(10100)

expect(commandReceiver0).toHaveBeenCalledTimes(0)

const hyperdeckInstances = HyperdeckConnection.Hyperdeck.getMockInstances()
expect(hyperdeckInstances).toHaveLength(3)

const hyperdeckMock: HyperdeckConnection.Hyperdeck = hyperdeckInstances[2]

const hyperdeckMockCommand = jest.fn(async () => {
return Promise.resolve()
})
hyperdeckMock.setMockCommandReceiver(hyperdeckMockCommand)

const deviceContainer = myConductor.getDevice('hyperdeck0')
device = deviceContainer!.device as ThreadedClass<HyperdeckDevice>

// Check that no commands has been scheduled:
expect(await device.queue).toHaveLength(0)
myConductor.setTimelineAndMappings([
{
id: 'obj0',
enable: {
start: 9000,
duration: 2000, // 11000
},
layer: 'hyperdeck0_transport',
content: {
deviceType: DeviceType.HYPERDECK,
type: TimelineContentTypeHyperdeck.TRANSPORT,

status: TransportStatus.PLAY,
speed: 100,
loop: false,
singleClip: true,
clipId: 1,
},
},
{
id: 'obj1',
enable: {
start: 10500,
duration: 2000,
},
layer: 'hyperdeck0_transport',
content: {
deviceType: DeviceType.HYPERDECK,
type: TimelineContentTypeHyperdeck.TRANSPORT,

status: TransportStatus.PREVIEW,
speed: 100,
loop: false,
singleClip: true,
clipId: null,
},
},
])

await mockTime.advanceTimeToTicks(10200)

expect(commandReceiver0).toHaveBeenCalledTimes(2)
expect(getMockCall(commandReceiver0, 0, 1)).toBeInstanceOf(PlayCommand)
expect(getMockCall(commandReceiver0, 0, 1)).toHaveProperty('speed', '100')
expect(getMockCall(commandReceiver0, 0, 1)).toHaveProperty('loop', false)
expect(getMockCall(commandReceiver0, 0, 1)).toHaveProperty('singleClip', true)
expect(getMockCall(commandReceiver0, 0, 2)).toBeTruthy() // context
expect(getMockCall(commandReceiver0, 1, 1)).toBeInstanceOf(GoToCommand)
expect(getMockCall(commandReceiver0, 1, 1)).toHaveProperty('clipId', 1)
expect(getMockCall(commandReceiver0, 1, 2)).toBeTruthy() // context
// also test the actual command sent to hyperdeck:
expect(hyperdeckMockCommand).toHaveBeenCalledTimes(2)
expect(getMockCall(hyperdeckMockCommand, 0, 0)).toBeInstanceOf(PlayCommand)
expect(getMockCall(hyperdeckMockCommand, 0, 0)).toMatchObject({
speed: '100',
loop: false,
singleClip: true,
})
expect(getMockCall(hyperdeckMockCommand, 1, 0)).toBeInstanceOf(GoToCommand)
expect(getMockCall(hyperdeckMockCommand, 1, 0)).toMatchObject({
clipId: 1,
})

myConductor.setTimelineAndMappings(myConductor.timeline) // Same timeline
await mockTime.advanceTimeToTicks(10400)
expect(hyperdeckMockCommand).toHaveBeenCalledTimes(2) // nothing has changed, so it should not be called again

await mockTime.advanceTimeToTicks(12000)

expect(commandReceiver0).toHaveBeenCalledTimes(3)
expect(getMockCall(commandReceiver0, 2, 1)).toBeInstanceOf(StopCommand)
expect(getMockCall(commandReceiver0, 2, 2)).toBeTruthy() // context

await mockTime.advanceTimeToTicks(13000)
expect(commandReceiver0).toHaveBeenCalledTimes(3)
// no new commands should have been sent, becuse obj2 is the same as obj1
})
})
Loading

0 comments on commit 50e9e15

Please sign in to comment.