Skip to content

Commit c2451c3

Browse files
authored
feat: multiviewer border and additional box properties (#169)
1 parent 87bb385 commit c2451c3

9 files changed

Lines changed: 243 additions & 6 deletions

src/atem.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,11 @@ import {
4242
FairlightAudioMonitorSolo,
4343
} from './state/fairlight'
4444
import { FairlightDynamicsResetProps } from './commands/Fairlight/common'
45-
import { MultiViewerPropertiesState } from './state/settings'
45+
import {
46+
MultiViewerPropertiesState,
47+
MultiViewerBorderColorState,
48+
MultiViewerWindowOverlayPropertiesState,
49+
} from './state/settings'
4650
import {
4751
calculateGenerateMultiviewerLabelProps,
4852
generateMultiviewerLabel,
@@ -569,6 +573,27 @@ export class Atem extends BasicAtem {
569573
command.updateProps(props)
570574
return this.sendCommand(command)
571575
}
576+
public async setMultiViewerBorderColor(color: MultiViewerBorderColorState, mv = 0): Promise<void> {
577+
const command = new Commands.MultiViewerBorderColorCommand(mv, color)
578+
return this.sendCommand(command)
579+
}
580+
public async setMultiViewerWindowOverlayProperties(
581+
props: Partial<MultiViewerWindowOverlayPropertiesState>,
582+
mv = 0,
583+
window = 0
584+
): Promise<void> {
585+
const command = new Commands.MultiViewerWindowOverlayPropertiesCommand(mv, window)
586+
command.updateProps(props)
587+
return this.sendCommand(command)
588+
}
589+
public async setMultiViewerWindowSafeAreaPattern(
590+
safeTitlePattern: Enums.SafeTitlePattern[],
591+
mv = 0,
592+
window = 0
593+
): Promise<void> {
594+
const command = new Commands.MultiViewerWindowOverlaySafeAreaPatternCommand(mv, window, safeTitlePattern)
595+
return this.sendCommand(command)
596+
}
572597

573598
public async setColorGeneratorColour(newProps: Partial<ColorGeneratorState>, index = 0): Promise<void> {
574599
const command = new Commands.ColorGeneratorCommand(index)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { SymmetricalCommand } from '../CommandBase'
2+
import { AtemState, AtemStateUtil, InvalidIdError } from '../../state'
3+
import type { MultiViewerBorderColorState } from '../../state/settings'
4+
5+
export class MultiViewerBorderColorCommand extends SymmetricalCommand<MultiViewerBorderColorState> {
6+
public static readonly rawName = 'MvBC'
7+
8+
public readonly multiViewerId: number
9+
10+
constructor(multiviewerId: number, color: MultiViewerBorderColorState) {
11+
super(color)
12+
13+
this.multiViewerId = multiviewerId
14+
}
15+
16+
public serialize(): Buffer {
17+
const buffer = Buffer.alloc(12)
18+
buffer.writeUInt8(this.multiViewerId, 0)
19+
buffer.writeUInt16BE(this.properties.red, 2)
20+
buffer.writeUInt16BE(this.properties.green, 4)
21+
buffer.writeUInt16BE(this.properties.blue, 6)
22+
buffer.writeUInt16BE(this.properties.alpha, 8)
23+
return buffer
24+
}
25+
26+
public static deserialize(rawCommand: Buffer): MultiViewerBorderColorCommand {
27+
const multiViewerId = rawCommand.readUInt8(0)
28+
29+
const color: MultiViewerBorderColorState = {
30+
red: rawCommand.readUInt16BE(2),
31+
green: rawCommand.readUInt16BE(4),
32+
blue: rawCommand.readUInt16BE(6),
33+
alpha: rawCommand.readUInt16BE(8),
34+
}
35+
36+
return new MultiViewerBorderColorCommand(multiViewerId, color)
37+
}
38+
39+
public applyToState(state: AtemState): string {
40+
if (!state.info.multiviewer || this.multiViewerId >= state.info.multiviewer.count) {
41+
throw new InvalidIdError('MultiViewer', this.multiViewerId)
42+
}
43+
44+
const multiviewer = AtemStateUtil.getMultiViewer(state, this.multiViewerId)
45+
multiviewer.borderColor = { ...this.properties }
46+
47+
return `settings.multiViewers.${this.multiViewerId}.borderColor`
48+
}
49+
}

src/commands/Settings/MultiViewerSourceCommand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export class MultiViewerSourceUpdateCommand extends DeserializedCommand<MultiVie
5656
const multiviewer = AtemStateUtil.getMultiViewer(state, this.multiViewerId)
5757
const currentWindow = multiviewer.windows[this.properties.windowIndex]
5858

59-
// The Constellation HD range has a bug where it sends this command for every window on every frame
59+
// The Constellation HD range had a bug where it sends this command for every window on every frame
6060
// This hides that from library consumers by doing a deep diff, when we usually do not.
6161
if (currentWindow && !isRunningInTests()) {
6262
let isChanged = false
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { DeserializedCommand, WritableCommand } from '../CommandBase'
2+
import { MultiViewerWindowOverlayPropertiesState } from '../../state/settings'
3+
import { AtemState, AtemStateUtil, InvalidIdError } from '../../state'
4+
5+
export class MultiViewerWindowOverlayPropertiesCommand extends WritableCommand<MultiViewerWindowOverlayPropertiesState> {
6+
public static MaskFlags = {
7+
labelVisible: 1 << 0,
8+
borderVisible: 1 << 1,
9+
}
10+
11+
public static readonly rawName = 'CMvO'
12+
13+
public readonly multiViewerId: number
14+
public readonly windowIndex: number
15+
16+
constructor(multiviewerId: number, windowIndex: number) {
17+
super()
18+
19+
this.multiViewerId = multiviewerId
20+
this.windowIndex = windowIndex
21+
}
22+
23+
public serialize(): Buffer {
24+
const buffer = Buffer.alloc(8)
25+
buffer.writeUInt8(this.multiViewerId, 0)
26+
buffer.writeUInt8(this.windowIndex || 0, 1)
27+
28+
let value = 0
29+
if (this.properties.labelVisible) value |= 0x01
30+
if (this.properties.borderVisible) value |= 0x02
31+
32+
buffer.writeUInt8(value, 3)
33+
buffer.writeUInt8(this.flag, 5)
34+
return buffer
35+
}
36+
}
37+
38+
export class MultiViewerWindowOverlayPropertiesUpdateCommand extends DeserializedCommand<MultiViewerWindowOverlayPropertiesState> {
39+
public static readonly rawName = 'MvOv'
40+
41+
public readonly multiViewerId: number
42+
public readonly windowIndex: number
43+
44+
constructor(multiviewerId: number, windowIndex: number, props: MultiViewerWindowOverlayPropertiesState) {
45+
super(props)
46+
47+
this.multiViewerId = multiviewerId
48+
this.windowIndex = windowIndex
49+
}
50+
51+
public static deserialize(rawCommand: Buffer): MultiViewerWindowOverlayPropertiesUpdateCommand {
52+
const multiViewerId = rawCommand.readUInt8(0)
53+
const windowIndex = rawCommand.readUInt8(1)
54+
55+
const values = rawCommand.readUInt8(3)
56+
57+
const props: MultiViewerWindowOverlayPropertiesState = {
58+
labelVisible: (values & 0x01) > 0,
59+
borderVisible: (values & 0x02) > 0,
60+
}
61+
62+
return new MultiViewerWindowOverlayPropertiesUpdateCommand(multiViewerId, windowIndex, props)
63+
}
64+
65+
public applyToState(state: AtemState): string {
66+
if (!state.info.multiviewer || this.multiViewerId >= state.info.multiviewer.count) {
67+
throw new InvalidIdError('MultiViewer', this.multiViewerId)
68+
}
69+
70+
const multiviewer = AtemStateUtil.getMultiViewer(state, this.multiViewerId)
71+
const window = multiviewer.windows[this.windowIndex]
72+
if (!window) {
73+
throw new InvalidIdError('MultiViewer Window', this.multiViewerId, this.windowIndex)
74+
}
75+
window.overlayProperties = this.properties
76+
77+
return `settings.multiViewers.${this.multiViewerId}.windows.${this.windowIndex}.overlayProperties`
78+
}
79+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { SymmetricalCommand } from '../CommandBase'
2+
import { AtemState, AtemStateUtil, InvalidIdError } from '../../state'
3+
import { SafeTitlePattern } from '../../enums'
4+
import { combineComponents, getComponents } from '../../lib/atemUtil'
5+
6+
export class MultiViewerWindowOverlaySafeAreaPatternCommand extends SymmetricalCommand<{
7+
safeTitlePattern: SafeTitlePattern[]
8+
}> {
9+
public static readonly rawName = 'StMw'
10+
11+
public readonly multiViewerId: number
12+
public readonly windowIndex: number
13+
14+
constructor(multiviewerId: number, windowIndex: number, safeTitlePattern: SafeTitlePattern[]) {
15+
super({ safeTitlePattern })
16+
17+
this.multiViewerId = multiviewerId
18+
this.windowIndex = windowIndex
19+
}
20+
21+
public serialize(): Buffer {
22+
const buffer = Buffer.alloc(4)
23+
buffer.writeUInt8(this.multiViewerId, 0)
24+
buffer.writeUInt8(this.windowIndex, 1)
25+
buffer.writeUInt8(combineComponents(this.properties.safeTitlePattern), 2)
26+
return buffer
27+
}
28+
29+
public static deserialize(rawCommand: Buffer): MultiViewerWindowOverlaySafeAreaPatternCommand {
30+
const multiViewerId = rawCommand.readUInt8(0)
31+
const windowIndex = rawCommand.readUInt8(1)
32+
const safeTitlePattern = getComponents(rawCommand.readUInt8(2)) as SafeTitlePattern[]
33+
34+
return new MultiViewerWindowOverlaySafeAreaPatternCommand(multiViewerId, windowIndex, safeTitlePattern)
35+
}
36+
37+
public applyToState(state: AtemState): string {
38+
if (!state.info.multiviewer || this.multiViewerId >= state.info.multiviewer.count) {
39+
throw new InvalidIdError('MultiViewer', this.multiViewerId)
40+
}
41+
42+
const multiviewer = AtemStateUtil.getMultiViewer(state, this.multiViewerId)
43+
const window = multiviewer.windows[this.windowIndex]
44+
if (!window) {
45+
throw new InvalidIdError('MultiViewer Window', this.multiViewerId, this.windowIndex)
46+
}
47+
window.safeTitlePattern = this.properties.safeTitlePattern
48+
49+
return `settings.multiViewers.${this.multiViewerId}.windows.${this.windowIndex}.safeTitlePattern`
50+
}
51+
}

src/commands/Settings/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
export * from './MediaPool'
2-
export * from './MultiViewerSourceCommand'
2+
export * from './MultiViewerBorderColorCommand'
33
export * from './MultiViewerPropertiesCommand'
4+
export * from './MultiViewerSourceCommand'
45
export * from './MultiViewerVuOpacityCommand'
5-
export * from './MultiViewerWindowVuMeterCommand'
6+
export * from './MultiViewerWindowOverlayPropertiesCommand'
67
export * from './MultiViewerWindowSafeAreaCommand'
8+
export * from './MultiViewerWindowSafeAreaTypeCommand'
9+
export * from './MultiViewerWindowVuMeterCommand'
710
export * from './VideoMode'

src/commands/__tests__/index.spec.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,12 @@ describe('Commands vs LibAtem', () => {
203203
!n.startsWith('TlSr') &&
204204
!n.startsWith('_VMC') &&
205205
!n.startsWith('FAMS') &&
206-
!n.startsWith('CFMS')
206+
!n.startsWith('CFMS') &&
207+
// new multiviewer border
208+
!n.startsWith('CMvO') &&
209+
!n.startsWith('MvBC') &&
210+
!n.startsWith('MvOv') &&
211+
!n.startsWith('StMw')
207212
)
208213

209214
knownNames.sort()

src/enums/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,3 +423,8 @@ export enum TimeMode {
423423
FreeRun = 0,
424424
TimeOfDay = 1,
425425
}
426+
427+
export enum SafeTitlePattern {
428+
Horizontal = 1,
429+
Vertical = 2,
430+
}

src/state/settings.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,47 @@
1-
import { VideoMode, MultiViewerLayout, TimeMode } from '../enums'
1+
import { VideoMode, MultiViewerLayout, TimeMode, SafeTitlePattern } from '../enums'
22

33
export interface MultiViewerSourceState {
44
source: number
55
readonly windowIndex: number
66
readonly supportsVuMeter: boolean
77
readonly supportsSafeArea: boolean
8+
// readonly supportsOverlayProperties: boolean
89
}
910

1011
export interface MultiViewerWindowState extends MultiViewerSourceState {
1112
safeTitle?: boolean
1213
audioMeter?: boolean
14+
15+
safeTitlePattern?: SafeTitlePattern[]
16+
overlayProperties?: MultiViewerWindowOverlayPropertiesState
1317
}
1418

1519
export interface MultiViewerPropertiesState {
1620
layout: MultiViewerLayout
1721
programPreviewSwapped: boolean
1822
}
23+
export interface MultiViewerBorderColorState {
24+
/** Red component 0-1000 */
25+
red: number
26+
/** Green component 0-1000 */
27+
green: number
28+
/** Blue component 0-1000 */
29+
blue: number
30+
/** Alpha component 0-1000 */
31+
alpha: number
32+
}
33+
export interface MultiViewerWindowOverlayPropertiesState {
34+
labelVisible: boolean
35+
borderVisible: boolean
36+
}
1937

2038
export interface MultiViewer {
2139
readonly index: number
2240
readonly windows: Array<MultiViewerWindowState | undefined>
2341
properties?: MultiViewerPropertiesState
2442
vuOpacity?: number
43+
44+
borderColor?: MultiViewerBorderColorState
2545
}
2646

2747
export interface MediaPool {

0 commit comments

Comments
 (0)