Skip to content

Commit 9036d5c

Browse files
authoredSep 10, 2024
feat: downloadStill from media pool (#167)
1 parent 5e62374 commit 9036d5c

File tree

8 files changed

+373
-3
lines changed

8 files changed

+373
-3
lines changed
 

‎src/atem.ts

+44-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { InputChannel } from './state/input'
1919
import { DownstreamKeyerGeneral, DownstreamKeyerMask } from './state/video/downstreamKeyers'
2020
import * as DT from './dataTransfer'
2121
import * as Util from './lib/atemUtil'
22-
import { getVideoModeInfo } from './lib/videoMode'
22+
import { VideoModeInfo, getVideoModeInfo } from './lib/videoMode'
2323
import * as Enums from './enums'
2424
import {
2525
ClassicAudioMonitorChannel,
@@ -56,6 +56,8 @@ import { TimeInfo } from './state/info'
5656
import { SomeAtemAudioLevels } from './state/levels'
5757
import { generateUploadBufferInfo, UploadBufferInfo } from './dataTransfer/dataTransferUploadBuffer'
5858
import { convertWAVToRaw } from './lib/converters/wavAudio'
59+
import { decodeRLE } from './lib/converters/rle'
60+
import { convertYUV422ToRGBA } from './lib/converters/yuv422ToRgba'
5961

6062
export interface AtemOptions {
6163
address?: string
@@ -147,6 +149,15 @@ export class BasicAtem extends EventEmitter<AtemEvents> {
147149
return this._state
148150
}
149151

152+
/**
153+
* Get the current videomode of the ATEM, if known
154+
*/
155+
get videoMode(): Readonly<VideoModeInfo> | undefined {
156+
if (!this.state) return undefined
157+
158+
return getVideoModeInfo(this.state.settings.videoMode)
159+
}
160+
150161
public async connect(address: string, port?: number): Promise<void> {
151162
return this.socket.connect(address, port)
152163
}
@@ -757,6 +768,38 @@ export class Atem extends BasicAtem {
757768
return this.sendCommand(command)
758769
}
759770

771+
/**
772+
* Download a still image from the ATEM media pool
773+
*
774+
* Note: This performs colour conversions in JS, which is not very CPU efficient. If performance is important,
775+
* consider using [@atem-connection/image-tools](https://www.npmjs.com/package/@atem-connection/image-tools) to
776+
* pre-convert the images with more optimal algorithms
777+
* @param index Still index to download
778+
* @param format The pixel format to return for the downloaded image. 'raw' passes through unchanged, and will be RLE encoded.
779+
* @returns Promise which returns the image once downloaded. If the still slot is not in use, this will throw
780+
*/
781+
public async downloadStill(index: number, format: 'raw' | 'rgba' | 'yuv' = 'rgba'): Promise<Buffer> {
782+
let rawBuffer = await this.dataTransferManager.downloadStill(index)
783+
784+
if (format === 'raw') {
785+
return rawBuffer
786+
}
787+
788+
if (!this.state) throw new Error('Unable to check current resolution')
789+
const resolution = getVideoModeInfo(this.state.settings.videoMode)
790+
if (!resolution) throw new Error('Failed to determine required resolution')
791+
792+
rawBuffer = decodeRLE(rawBuffer, resolution.width * resolution.height * 4)
793+
794+
switch (format) {
795+
case 'yuv':
796+
return rawBuffer
797+
case 'rgba':
798+
default:
799+
return convertYUV422ToRGBA(resolution.width, resolution.height, rawBuffer)
800+
}
801+
}
802+
760803
/**
761804
* Upload a still image to the ATEM media pool
762805
*

‎src/commands/DataTransfer/DataTransferDownloadRequestCommand.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class DataTransferDownloadRequestCommand extends BasicWritableCommand<Dat
1616
buffer.writeUInt16BE(this.properties.transferStoreId, 2)
1717
buffer.writeUInt16BE(this.properties.transferIndex, 6)
1818

19-
buffer.writeUInt8(this.properties.transferType, 8)
19+
buffer.writeUInt16BE(this.properties.transferType, 8)
2020

2121
return buffer
2222
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
DataTransferAckCommand,
3+
DataTransferCompleteCommand,
4+
DataTransferDataCommand,
5+
DataTransferDownloadRequestCommand,
6+
DataTransferErrorCommand,
7+
ErrorCode,
8+
} from '../commands/DataTransfer'
9+
import { IDeserializedCommand } from '../commands/CommandBase'
10+
import { DataTransfer, ProgressTransferResult, DataTransferState } from './dataTransfer'
11+
12+
// TODO - this should be reimplemented on top of a generic DataTransferDownloadBuffer class
13+
export class DataTransferDownloadStill extends DataTransfer<Buffer> {
14+
#data: Buffer[] = []
15+
16+
constructor(public readonly stillIndex: number) {
17+
super()
18+
}
19+
20+
public async startTransfer(transferId: number): Promise<ProgressTransferResult> {
21+
const command = new DataTransferDownloadRequestCommand({
22+
transferId: transferId,
23+
transferStoreId: 0x00,
24+
transferIndex: this.stillIndex,
25+
transferType: 0x00f9,
26+
})
27+
28+
return {
29+
newState: DataTransferState.Ready,
30+
commands: [command],
31+
}
32+
}
33+
34+
public async handleCommand(
35+
command: IDeserializedCommand,
36+
oldState: DataTransferState
37+
): Promise<ProgressTransferResult> {
38+
if (command instanceof DataTransferErrorCommand) {
39+
switch (command.properties.errorCode) {
40+
case ErrorCode.Retry:
41+
return this.restartTransfer(command.properties.transferId)
42+
43+
case ErrorCode.NotFound:
44+
this.abort(new Error('Invalid download'))
45+
46+
return {
47+
newState: DataTransferState.Finished,
48+
commands: [],
49+
}
50+
default:
51+
// Abort the transfer.
52+
this.abort(new Error(`Unknown error ${command.properties.errorCode}`))
53+
54+
return {
55+
newState: DataTransferState.Finished,
56+
commands: [],
57+
}
58+
}
59+
} else if (command instanceof DataTransferDataCommand) {
60+
this.#data.push(command.properties.body)
61+
62+
// todo - have we received all data? maybe check if the command.body < max_len
63+
64+
return {
65+
newState: oldState,
66+
commands: [
67+
new DataTransferAckCommand({
68+
transferId: command.properties.transferId,
69+
transferIndex: this.stillIndex,
70+
}),
71+
],
72+
}
73+
} else if (command instanceof DataTransferCompleteCommand) {
74+
this.resolvePromise(Buffer.concat(this.#data))
75+
76+
return {
77+
newState: DataTransferState.Finished,
78+
commands: [],
79+
}
80+
}
81+
82+
return { newState: oldState, commands: [] }
83+
}
84+
}

‎src/dataTransfer/index.ts

+7
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { DataTransferUploadMacro } from './dataTransferUploadMacro'
1111
import { LockObtainedCommand, LockStateUpdateCommand } from '../commands/DataTransfer'
1212
import debug0 from 'debug'
1313
import type { UploadBufferInfo } from './dataTransferUploadBuffer'
14+
import { DataTransferDownloadStill } from './dataTransferDownloadStill'
1415

1516
const MAX_PACKETS_TO_SEND_PER_TICK = 50
1617
const MAX_TRANSFER_INDEX = (1 << 16) - 1 // Inclusive maximum
@@ -170,6 +171,12 @@ export class DataTransferManager {
170171
}
171172
}
172173

174+
public async downloadStill(index: number): Promise<Buffer> {
175+
const transfer = new DataTransferDownloadStill(index)
176+
177+
return this.#stillsLock.enqueue(transfer)
178+
}
179+
173180
public async uploadStill(index: number, data: UploadBufferInfo, name: string, description: string): Promise<void> {
174181
const transfer = new DataTransferUploadStill(index, data, name, description)
175182
return this.#stillsLock.enqueue(transfer)

‎src/lib/converters/__tests__/rle.spec.ts

+136-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { encodeRLE } from '../rle'
1+
import { decodeRLE, encodeRLE } from '../rle'
22

33
describe('encodeRLE', () => {
44
test('no repetitions', () => {
@@ -134,3 +134,138 @@ abababababababab`
134134
expect(encoded.toString('hex')).toEqual(expectation)
135135
})
136136
})
137+
138+
describe('decodeRLE', () => {
139+
test('no repetitions', () => {
140+
const source = `abababababababab\
141+
cdcdcdcdcdcdcdcd\
142+
abababababababab\
143+
cdcdcdcdcdcdcdcd`
144+
const decoded = decodeRLE(Buffer.from(source, 'hex'), source.length / 2)
145+
expect(decoded.toString('hex')).toEqual(source)
146+
})
147+
148+
test('two repetitions', () => {
149+
const source = `abababababababab\
150+
abababababababab\
151+
cdcdcdcdcdcdcdcd\
152+
0000000000000000\
153+
1111111111111111`
154+
const decoded = decodeRLE(Buffer.from(source, 'hex'), source.length / 2)
155+
expect(decoded.toString('hex')).toEqual(source)
156+
})
157+
158+
test('three repetitions', () => {
159+
const source = `abababababababab\
160+
abababababababab\
161+
abababababababab\
162+
cdcdcdcdcdcdcdcd\
163+
0000000000000000\
164+
1111111111111111`
165+
const decoded = decodeRLE(Buffer.from(source, 'hex'), source.length / 2)
166+
expect(decoded.toString('hex')).toEqual(source)
167+
})
168+
169+
test('four repetitions', () => {
170+
const source = `fefefefefefefefe\
171+
0000000000000004\
172+
abababababababab\
173+
cdcdcdcdcdcdcdcd\
174+
0000000000000000\
175+
1111111111111111`
176+
const expectation = `abababababababab\
177+
abababababababab\
178+
abababababababab\
179+
abababababababab\
180+
cdcdcdcdcdcdcdcd\
181+
0000000000000000\
182+
1111111111111111`
183+
const decoded = decodeRLE(Buffer.from(source, 'hex'), expectation.length / 2)
184+
expect(decoded.toString('hex')).toEqual(expectation)
185+
})
186+
187+
test('five repetitions at the beginning', () => {
188+
const source = `fefefefefefefefe\
189+
0000000000000005\
190+
abababababababab\
191+
cdcdcdcdcdcdcdcd\
192+
0000000000000000\
193+
1111111111111111`
194+
const expectation = `abababababababab\
195+
abababababababab\
196+
abababababababab\
197+
abababababababab\
198+
abababababababab\
199+
cdcdcdcdcdcdcdcd\
200+
0000000000000000\
201+
1111111111111111`
202+
const decoded = decodeRLE(Buffer.from(source, 'hex'), expectation.length / 2)
203+
expect(decoded.toString('hex')).toEqual(expectation)
204+
})
205+
206+
test('five repetitions in the midddle', () => {
207+
const source = `2323232323232323\
208+
fefefefefefefefe\
209+
0000000000000005\
210+
abababababababab\
211+
cdcdcdcdcdcdcdcd\
212+
0000000000000000\
213+
1111111111111111`
214+
const expectation = `2323232323232323\
215+
abababababababab\
216+
abababababababab\
217+
abababababababab\
218+
abababababababab\
219+
abababababababab\
220+
cdcdcdcdcdcdcdcd\
221+
0000000000000000\
222+
1111111111111111`
223+
const decoded = decodeRLE(Buffer.from(source, 'hex'), expectation.length / 2)
224+
expect(decoded.toString('hex')).toEqual(expectation)
225+
})
226+
227+
test('five repetitions in the midddle #2', () => {
228+
const source = `2323232323232323\
229+
fefefefefefefefe\
230+
0000000000000005\
231+
abababababababab\
232+
cdcdcdcdcdcdcdcd`
233+
const expectation = `2323232323232323\
234+
abababababababab\
235+
abababababababab\
236+
abababababababab\
237+
abababababababab\
238+
abababababababab\
239+
cdcdcdcdcdcdcdcd`
240+
const decoded = decodeRLE(Buffer.from(source, 'hex'), expectation.length / 2)
241+
expect(decoded.toString('hex')).toEqual(expectation)
242+
})
243+
244+
test('five repetitions at the end', () => {
245+
const source = `2323232323232323\
246+
fefefefefefefefe\
247+
0000000000000005\
248+
abababababababab`
249+
const expectation = `2323232323232323\
250+
abababababababab\
251+
abababababababab\
252+
abababababababab\
253+
abababababababab\
254+
abababababababab`
255+
const decoded = decodeRLE(Buffer.from(source, 'hex'), expectation.length / 2)
256+
expect(decoded.toString('hex')).toEqual(expectation)
257+
})
258+
259+
test('only five repetitions', () => {
260+
const source = `fefefefefefefefe\
261+
0000000000000005\
262+
abababababababab`
263+
const expectation = `abababababababab\
264+
abababababababab\
265+
abababababababab\
266+
abababababababab\
267+
abababababababab`
268+
const decoded = decodeRLE(Buffer.from(source, 'hex'), expectation.length / 2)
269+
expect(decoded.toString('hex')).toEqual(expectation)
270+
})
271+
})

‎src/lib/converters/colorConstants.ts

+6
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export interface ColorConvertConstants {
1313
readonly YOffset: number
1414
readonly CbCrOffset: number
1515

16+
readonly KRKRioKG: number
17+
readonly KBKBioKG: number
18+
1619
readonly KRoKBi: number
1720
readonly KGoKBi: number
1821
readonly KBoKRi: number
@@ -44,6 +47,9 @@ function createColorConvertConstants(KR: number, KB: number): ColorConvertConsta
4447
YOffset: 16 << 8,
4548
CbCrOffset: 128 << 8,
4649

50+
KRKRioKG: (KR * KRi * 2) / KG,
51+
KBKBioKG: (KB * KBi * 2) / KG,
52+
4753
KRoKBi: (KR / KBi) * HalfCbCrRange,
4854
KGoKBi: (KG / KBi) * HalfCbCrRange,
4955
KBoKRi: (KB / KRi) * HalfCbCrRange,

‎src/lib/converters/rle.ts

+31
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,34 @@ export function encodeRLE(data: Buffer): Buffer {
5151

5252
return result.slice(0, resultOffset + 8)
5353
}
54+
55+
export function decodeRLE(data: Buffer, fullSize: number): Buffer {
56+
const result = Buffer.alloc(fullSize)
57+
58+
let resultOffset = -8
59+
60+
for (let sourceOffset = 0; sourceOffset < data.length; sourceOffset += 8) {
61+
const block = data.readBigUInt64BE(sourceOffset)
62+
63+
// read a header, start a repeating block
64+
if (block === RLE_HEADER) {
65+
// Read the count
66+
sourceOffset += 8
67+
const repeatCount = Number(data.readBigUInt64BE(sourceOffset))
68+
69+
// Read the repeated sample
70+
sourceOffset += 8
71+
const repeatBlock = data.readBigUInt64BE(sourceOffset)
72+
73+
// Write to the output
74+
for (let i = 0; i < repeatCount; i++) {
75+
result.writeBigUInt64BE(repeatBlock, (resultOffset += 8))
76+
}
77+
} else {
78+
// No RLE, repeat unchanged
79+
result.writeBigUInt64BE(block, (resultOffset += 8))
80+
}
81+
}
82+
83+
return result
84+
}

0 commit comments

Comments
 (0)
Failed to load comments.