Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ export interface MappingAtemMixEffect {
mappingType: MappingAtemType.MixEffect
}

export interface MappingAtemUpStreamKeyer {
me: number
usk: number
mappingType: MappingAtemType.UpStreamKeyer
}

export interface MappingAtemDownStreamKeyer {
index: number
mappingType: MappingAtemType.DownStreamKeyer
Expand Down Expand Up @@ -79,6 +85,7 @@ export interface MappingAtemColorGenerator {
export enum MappingAtemType {
ControlValue = 'controlValue',
MixEffect = 'mixEffect',
UpStreamKeyer = 'upStreamKeyer',
DownStreamKeyer = 'downStreamKeyer',
SuperSourceBox = 'superSourceBox',
Auxilliary = 'auxilliary',
Expand All @@ -90,7 +97,7 @@ export enum MappingAtemType {
ColorGenerator = 'colorGenerator',
}

export type SomeMappingAtem = MappingAtemControlValue | MappingAtemMixEffect | MappingAtemDownStreamKeyer | MappingAtemSuperSourceBox | MappingAtemAuxilliary | MappingAtemMediaPlayer | MappingAtemSuperSourceProperties | MappingAtemAudioChannel | MappingAtemMacroPlayer | MappingAtemAudioRouting | MappingAtemColorGenerator
export type SomeMappingAtem = MappingAtemControlValue | MappingAtemMixEffect | MappingAtemUpStreamKeyer | MappingAtemDownStreamKeyer | MappingAtemSuperSourceBox | MappingAtemAuxilliary | MappingAtemMediaPlayer | MappingAtemSuperSourceProperties | MappingAtemAudioChannel | MappingAtemMacroPlayer | MappingAtemAudioRouting | MappingAtemColorGenerator

export interface RunMacroPayload {
macroIndex: number
Expand Down
50 changes: 50 additions & 0 deletions packages/timeline-state-resolver-types/src/integrations/atem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { DeviceType } from '..'
export enum TimelineContentTypeAtem { // Atem-state
ControlValue = 'controlValue',
ME = 'me',
USK = 'usk',
DSK = 'dsk',
AUX = 'aux',
SSRC = 'ssrc',
Expand Down Expand Up @@ -122,6 +123,7 @@ export interface AtemTransitionSettings {
export type TimelineContentAtemAny =
| TimelineContentAtemControlValue
| TimelineContentAtemME
| TimelineContentAtemUSK
| TimelineContentAtemDSK
| TimelineContentAtemAUX
| TimelineContentAtemSsrc
Expand Down Expand Up @@ -180,6 +182,11 @@ export interface TimelineContentAtemME extends TimelineContentAtemBase {
/** Settings for mix rate, wipe style */
transitionSettings?: AtemTransitionSettings

/**
* @deprecated Upstream Keyers should now be controlled using separate timeline objects
* with `type: TimelineContentTypeAtem.USK` and `MappingAtemType.UpStreamKeyer` mappings.
* This legacy method of controlling USKs via M/E properties will be removed in a future version.
*/
upstreamKeyers?: {
readonly upstreamKeyerId: number
onAir?: boolean
Expand Down Expand Up @@ -223,6 +230,49 @@ export interface TimelineContentAtemME extends TimelineContentAtemBase {
}[]
}
}
export interface TimelineContentAtemUSK extends TimelineContentAtemBase {
type: TimelineContentTypeAtem.USK
usk: {
onAir?: boolean
/** 0: Luma, 1: Chroma, 2: Pattern, 3: DVE */
mixEffectKeyType?: number
/** Use flying key */
flyEnabled?: boolean
/** Fill */
fillSource?: number
/** Key */
cutSource?: number
/** Mask keyer */
maskEnabled?: boolean
/** -9000 -> 9000 */
maskTop?: number
/** -9000 -> 9000 */
maskBottom?: number
/** -16000 -> 16000 */
maskLeft?: number
/** -16000 -> 16000 */
maskRight?: number

dveSettings?: AtemDVESettings
// chromaSettings: UpstreamKeyerChromaSettings;
// patternSettings: UpstreamKeyerPatternSettings;
flyKeyframes?: [AtemFlyKeyframe | undefined, AtemFlyKeyframe | undefined]
flyProperties?: {
isAtKeyFrame?: FlyKeyKeyFrame
runToInfiniteIndex?: FlyKeyDirection
}
lumaSettings?: {
/** Premultiply key */
preMultiplied?: boolean
/** 0-1000 */
clip?: number
/** 0-1000 */
gain?: number
/** Invert key */
invert?: boolean
}
}
}
export interface TimelineContentAtemDSK extends TimelineContentAtemBase {
type: TimelineContentTypeAtem.DSK
dsk: {
Expand Down
36 changes: 36 additions & 0 deletions packages/timeline-state-resolver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,45 @@ Cut to source 2 on ME1
}
}
}
```

### Upstream Keyer (USK)

Turn on Upstream Keyer 1 on ME1 (M/E 0 in 0-indexed TSR)

```typescript
// Mapping:
{
myLayerUSK1: {
device: DeviceType.ATEM,
deviceId: 'myAtem',
mappingType: MappingAtemType.UpStreamKeyer,
me: 0, // ME1 (0-indexed)
keyer: 0 // USK1 (0-indexed)
}
}
// Timeline:
{
id: 'usk1_on',
enable: {
start: 'now',
duration: 10000
},
layer: 'myLayerUSK1',
content: {
deviceType: DeviceType.ATEM,
type: TimelineContentTypeAtem.USK,
usk: {
onAir: true,
fillSource: 3,
cutSource: 4
}
}
}
```

**Note:** Each USK should have its own layer with a dedicated `UpStreamKeyer` mapping. The legacy method of controlling USKs via the M/E timeline object's `upstreamKeyers` property is deprecated and will be removed in a future version.

## Blackmagic Design Hyperdeck

### Record a clip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,29 @@
"required": ["index"],
"additionalProperties": false
},
"upStreamKeyer": {
"type": "object",
"properties": {
"me": {
"type": "integer",
"ui:title": "Index",
"ui:summaryTitle": "USK",
"default": 0,
"min": 0,
"ui:zeroBased": true
},
"usk": {
"type": "integer",
"ui:title": "Index",
"ui:summaryTitle": "USK",
"default": 0,
"min": 0,
"ui:zeroBased": true
}
},
"required": ["me", "usk"],
"additionalProperties": false
},
"downStreamKeyer": {
"type": "object",
"properties": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ describe('AtemStateBuilder', () => {
expect(deviceState1).toEqual(expectedState)
})

test('Upstream keyer', async () => {
test('Upstream keyer (legacy)', async () => {
const mockState1: DeviceTimelineStateObject<TSRTimelineContent>[] = [
makeDeviceTimelineStateObject({
id: 'obj0',
Expand Down Expand Up @@ -159,7 +159,7 @@ describe('AtemStateBuilder', () => {
expect(deviceState1).toEqual(expectedState)
})

test('Upstream keyer: Uses upstreamKeyerId as index', async () => {
test('Upstream keyer: Uses upstreamKeyerId as index (legacy)', async () => {
const mockState1: DeviceTimelineStateObject<TSRTimelineContent>[] = [
makeDeviceTimelineStateObject({
id: 'obj0',
Expand Down Expand Up @@ -204,6 +204,109 @@ describe('AtemStateBuilder', () => {
const deviceState1 = AtemStateBuilder.fromTimeline(mockState1, myLayerMapping)
expect(deviceState1).toEqual(expectedState)
})

test('Upstream keyer (new)', async () => {
const myLayerMappingUSK: Mappings = {
myLayer0: {
device: DeviceType.ATEM,
deviceId: 'myAtem',
options: {
mappingType: MappingAtemType.UpStreamKeyer,
me: 0,
usk: 0,
},
},
}

const mockState1: DeviceTimelineStateObject<TSRTimelineContent>[] = [
makeDeviceTimelineStateObject({
id: 'obj0',
enable: {
start: -1000, // 1 seconds ago
duration: 2000,
},
layer: 'myLayer0',
content: {
deviceType: DeviceType.ATEM,
type: TimelineContentTypeAtem.USK,
usk: {
lumaSettings: {
preMultiplied: false,
clip: 300,
gain: 2,
invert: true,
},
},
},
}),
]

const expectedState = AtemConnection.AtemStateUtil.Create() as InternalAtemConnectionState
const expectedMixEffect = AtemConnection.AtemStateUtil.getMixEffect(expectedState, 0)
AtemConnection.AtemStateUtil.getUpstreamKeyer(expectedMixEffect, 0).lumaSettings = {
preMultiplied: false,
clip: 300,
gain: 2,
invert: true,
}

expectedState['controlValues'] = { 'video.mixEffects.0.keyer.0': '0' }

const deviceState1 = AtemStateBuilder.fromTimeline(mockState1, myLayerMappingUSK)
expect(deviceState1).toEqual(expectedState)
})

test('Upstream keyer: Uses usk index from mapping (new)', async () => {
const myLayerMappingUSK: Mappings = {
myLayer0: {
device: DeviceType.ATEM,
deviceId: 'myAtem',
options: {
mappingType: MappingAtemType.UpStreamKeyer,
me: 0,
usk: 2,
},
},
}

const mockState1: DeviceTimelineStateObject<TSRTimelineContent>[] = [
makeDeviceTimelineStateObject({
id: 'obj0',
enable: {
start: -1000, // 1 seconds ago
duration: 2000,
},
layer: 'myLayer0',
content: {
deviceType: DeviceType.ATEM,
type: TimelineContentTypeAtem.USK,
usk: {
lumaSettings: {
preMultiplied: false,
clip: 300,
gain: 2,
invert: true,
},
},
},
}),
]

const expectedState = AtemConnection.AtemStateUtil.Create() as InternalAtemConnectionState
const expectedMixEffect = AtemConnection.AtemStateUtil.getMixEffect(expectedState, 0)
AtemConnection.AtemStateUtil.getUpstreamKeyer(expectedMixEffect, 2).lumaSettings = {
preMultiplied: false,
clip: 300,
gain: 2,
invert: true,
}
expect(expectedMixEffect.upstreamKeyers).toHaveLength(3)

expectedState.controlValues = { 'video.mixEffects.0.keyer.2': '0' }

const deviceState1 = AtemStateBuilder.fromTimeline(mockState1, myLayerMappingUSK)
expect(deviceState1).toEqual(expectedState)
})
})

describe('Downstream keyer', () => {
Expand Down Expand Up @@ -708,4 +811,77 @@ describe('AtemStateBuilder', () => {
expect(deviceState1).toEqual(expectedState)
})
})

describe('USK Conflict Detection', () => {
test('Detects conflict when both legacy and new USK control are used', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()

const myLayerMapping: Mappings = {
legacyLayer: {
device: DeviceType.ATEM,
deviceId: 'myAtem',
options: {
mappingType: MappingAtemType.MixEffect,
index: 0,
},
},
newLayer: {
device: DeviceType.ATEM,
deviceId: 'myAtem',
options: {
mappingType: MappingAtemType.UpStreamKeyer,
me: 0,
usk: 0,
},
},
}

const mockState: DeviceTimelineStateObject<TSRTimelineContent>[] = [
makeDeviceTimelineStateObject({
id: 'legacyObj',
enable: {
start: 0,
duration: 2000,
},
layer: 'legacyLayer',
content: {
deviceType: DeviceType.ATEM,
type: TimelineContentTypeAtem.ME,
me: {
upstreamKeyers: [
{
upstreamKeyerId: 0,
onAir: true,
},
],
},
},
}),
makeDeviceTimelineStateObject({
id: 'newObj',
enable: {
start: 0,
duration: 2000,
},
layer: 'newLayer',
content: {
deviceType: DeviceType.ATEM,
type: TimelineContentTypeAtem.USK,
usk: {
onAir: true,
},
},
}),
]

AtemStateBuilder.fromTimeline(mockState, myLayerMapping)

expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('Conflict detected! M/E 0 USK 0'))
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('both legacy (M/E embedded) and new (separate layer) methods')
)

consoleErrorSpy.mockRestore()
})
})
})
Loading
Loading