diff --git a/packages/timeline-state-resolver/src/__mocks__/net.ts b/packages/timeline-state-resolver/src/__mocks__/net.ts
index cd31e5f89..00f94f8ea 100644
--- a/packages/timeline-state-resolver/src/__mocks__/net.ts
+++ b/packages/timeline-state-resolver/src/__mocks__/net.ts
@@ -67,7 +67,7 @@ export class Socket extends EventEmitter {
public mockClose() {
this.setClosed()
}
- public mockData(data: Buffer) {
+ public mockData(data: string) {
this.emit('data', data)
}
diff --git a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixResponseStreamReader.spec.ts b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixResponseStreamReader.spec.ts
index 9e919c271..a071ae971 100644
--- a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixResponseStreamReader.spec.ts
+++ b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixResponseStreamReader.spec.ts
@@ -1,13 +1,20 @@
import { VMixResponseStreamReader } from '../vMixResponseStreamReader'
describe('VMixResponseStreamReader', () => {
+ test('the helper uses byte length of strings', () => {
+ // this is a meta-test for the helper used in unit tests below, to assert that the data length is in bytes (utf-8), not characters
+ expect(makeXmlMessage('abc')).toBe('XML 18\r\nabc\r\n')
+ expect(makeXmlMessage('abc¾')).toBe('XML 20\r\nabc¾\r\n')
+ expect(makeXmlMessage('abc🚀🚀')).toBe('XML 26\r\nabc🚀🚀\r\n')
+ })
+
it('processes a complete message', async () => {
const reader = new VMixResponseStreamReader()
const onMessage = jest.fn()
reader.on('response', onMessage)
- reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n'))
+ reader.processIncomingData('VERSION OK 27.0.0.49\r\n')
expect(onMessage).toHaveBeenCalledTimes(1)
expect(onMessage).toHaveBeenCalledWith(
@@ -24,8 +31,8 @@ describe('VMixResponseStreamReader', () => {
const onMessage = jest.fn()
reader.on('response', onMessage)
- reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n'))
- reader.processIncomingData(Buffer.from('FUNCTION OK Take\r\n'))
+ reader.processIncomingData('VERSION OK 27.0.0.49\r\n')
+ reader.processIncomingData('FUNCTION OK Take\r\n')
expect(onMessage).toHaveBeenCalledTimes(2)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -51,8 +58,8 @@ describe('VMixResponseStreamReader', () => {
const onMessage = jest.fn()
reader.on('response', onMessage)
- reader.processIncomingData(Buffer.from('VERSION O'))
- reader.processIncomingData(Buffer.from('K 27.0.0.49\r\n'))
+ reader.processIncomingData('VERSION O')
+ reader.processIncomingData('K 27.0.0.49\r\n')
expect(onMessage).toHaveBeenCalledTimes(1)
expect(onMessage).toHaveBeenCalledWith(
@@ -69,9 +76,9 @@ describe('VMixResponseStreamReader', () => {
const onMessage = jest.fn()
reader.on('response', onMessage)
- reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n'))
- reader.processIncomingData(Buffer.from('FUNCTION'))
- reader.processIncomingData(Buffer.from(' ER Error message\r\n'))
+ reader.processIncomingData('VERSION OK 27.0.0.49\r\n')
+ reader.processIncomingData('FUNCTION')
+ reader.processIncomingData(' ER Error message\r\n')
expect(onMessage).toHaveBeenCalledTimes(2)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -97,11 +104,11 @@ describe('VMixResponseStreamReader', () => {
const onMessage = jest.fn()
reader.on('response', onMessage)
- reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49'))
- reader.processIncomingData(Buffer.from('\r\n'))
- reader.processIncomingData(Buffer.from('FUNCTION'))
- reader.processIncomingData(Buffer.from(' ER Error message\r'))
- reader.processIncomingData(Buffer.from('\n'))
+ reader.processIncomingData('VERSION OK 27.0.0.49')
+ reader.processIncomingData('\r\n')
+ reader.processIncomingData('FUNCTION')
+ reader.processIncomingData(' ER Error message\r')
+ reader.processIncomingData('\n')
expect(onMessage).toHaveBeenCalledTimes(2)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -127,7 +134,7 @@ describe('VMixResponseStreamReader', () => {
const onMessage = jest.fn()
reader.on('response', onMessage)
- reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\nFUNCTION ER Error message\r\n'))
+ reader.processIncomingData('VERSION OK 27.0.0.49\r\nFUNCTION ER Error message\r\n')
expect(onMessage).toHaveBeenCalledTimes(2)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -153,8 +160,8 @@ describe('VMixResponseStreamReader', () => {
const onMessage = jest.fn()
reader.on('response', onMessage)
- reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\nFUNCTION E'))
- reader.processIncomingData(Buffer.from('R Error message\r\n'))
+ reader.processIncomingData('VERSION OK 27.0.0.49\r\nFUNCTION E')
+ reader.processIncomingData('R Error message\r\n')
expect(onMessage).toHaveBeenCalledTimes(2)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -180,9 +187,9 @@ describe('VMixResponseStreamReader', () => {
const onMessage = jest.fn()
reader.on('response', onMessage)
- reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\nFUNCTION E'))
- reader.processIncomingData(Buffer.from('R Error message\r\nFUNCTION OK T'))
- reader.processIncomingData(Buffer.from('ake\r\n'))
+ reader.processIncomingData('VERSION OK 27.0.0.49\r\nFUNCTION E')
+ reader.processIncomingData('R Error message\r\nFUNCTION OK T')
+ reader.processIncomingData('ake\r\n')
expect(onMessage).toHaveBeenCalledTimes(3)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -218,7 +225,28 @@ describe('VMixResponseStreamReader', () => {
const xmlString =
'27.0.0.49HDC:\\preset.vmix'
- reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString)))
+ reader.processIncomingData(makeXmlMessage(xmlString))
+
+ expect(onMessage).toHaveBeenCalledTimes(1)
+ expect(onMessage).toHaveBeenNthCalledWith(
+ 1,
+ expect.objectContaining({
+ command: 'XML',
+ response: 'OK',
+ body: xmlString,
+ })
+ )
+ })
+
+ it('processes a message with data containing multi-byte characters', async () => {
+ const reader = new VMixResponseStreamReader()
+
+ const onMessage = jest.fn()
+ reader.on('response', onMessage)
+
+ const xmlString =
+ '27.0.0.49HDC:\\🚀\\preset3¾.vmix'
+ reader.processIncomingData(makeXmlMessage(xmlString))
expect(onMessage).toHaveBeenCalledTimes(1)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -241,7 +269,7 @@ describe('VMixResponseStreamReader', () => {
const xmlString =
'\r\n27.0.0.49\r\nHD\r\nC:\\preset.vmix\r\n\r\n'
- reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString)))
+ reader.processIncomingData(makeXmlMessage(xmlString))
expect(onMessage).toHaveBeenCalledTimes(1)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -265,7 +293,7 @@ describe('VMixResponseStreamReader', () => {
const xmlMessage = makeXmlMessage(xmlString)
splitAtIndices(xmlMessage, [2, 10, 25, 40]).forEach((fragment) => {
expect(fragment.length).toBeGreaterThan(0)
- reader.processIncomingData(Buffer.from(fragment))
+ reader.processIncomingData(fragment)
})
expect(onMessage).toHaveBeenCalledTimes(1)
@@ -289,7 +317,7 @@ describe('VMixResponseStreamReader', () => {
'27.0.0.49HDC:\\preset.vmix'
const xmlString2 = '25.0.0.14K'
- reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString) + makeXmlMessage(xmlString2)))
+ reader.processIncomingData(makeXmlMessage(xmlString) + makeXmlMessage(xmlString2))
expect(onMessage).toHaveBeenCalledTimes(2)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -320,8 +348,8 @@ describe('VMixResponseStreamReader', () => {
'27.0.0.49HDC:\\preset.vmix'
const xmlString2 = '25.0.0.14K'
- reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString)))
- reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString2)))
+ reader.processIncomingData(makeXmlMessage(xmlString))
+ reader.processIncomingData(makeXmlMessage(xmlString2))
expect(onMessage).toHaveBeenCalledTimes(2)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -352,9 +380,9 @@ describe('VMixResponseStreamReader', () => {
'27.0.0.49HDC:\\preset.vmix'
const xmlString2 = '25.0.0.14K'
- reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString).substring(0, 44)))
+ reader.processIncomingData(makeXmlMessage(xmlString).substring(0, 44))
reader.reset()
- reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString2)))
+ reader.processIncomingData(makeXmlMessage(xmlString2))
expect(onMessage).toHaveBeenCalledTimes(1)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -377,8 +405,8 @@ describe('VMixResponseStreamReader', () => {
const onError = jest.fn()
reader.on('error', onError)
- reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n'))
- reader.processIncomingData(Buffer.from('FUNCTION OK Take\r\n'))
+ reader.processIncomingData('VERSION OK 27.0.0.49\r\n')
+ reader.processIncomingData('FUNCTION OK Take\r\n')
expect(onMessage).toHaveBeenCalledTimes(2)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -408,9 +436,9 @@ describe('VMixResponseStreamReader', () => {
const onError = jest.fn()
reader.on('error', onError)
- reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n'))
- reader.processIncomingData(Buffer.from('\r\n'))
- reader.processIncomingData(Buffer.from('FUNCTION OK Take\r\n'))
+ reader.processIncomingData('VERSION OK 27.0.0.49\r\n')
+ reader.processIncomingData('\r\n')
+ reader.processIncomingData('FUNCTION OK Take\r\n')
expect(onMessage).toHaveBeenCalledTimes(2)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -440,9 +468,9 @@ describe('VMixResponseStreamReader', () => {
const onError = jest.fn()
reader.on('error', onError)
- reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n'))
- reader.processIncomingData(Buffer.from('WASSUP\r\n'))
- reader.processIncomingData(Buffer.from('FUNCTION OK Take\r\n'))
+ reader.processIncomingData('VERSION OK 27.0.0.49\r\n')
+ reader.processIncomingData('WASSUP\r\n')
+ reader.processIncomingData('FUNCTION OK Take\r\n')
expect(onMessage).toHaveBeenCalledTimes(2)
expect(onMessage).toHaveBeenNthCalledWith(
@@ -466,7 +494,8 @@ describe('VMixResponseStreamReader', () => {
})
function makeXmlMessage(xmlString: string): string {
- return `XML ${xmlString.length + 2}\r\n${xmlString}\r\n`
+ // the length of the data is in bytes, not characters!
+ return `XML ${Buffer.byteLength(xmlString, 'utf-8') + 2}\r\n${xmlString}\r\n`
}
function splitAtIndices(text: string, indices: number[]) {
diff --git a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vmixMock.ts b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vmixMock.ts
index f298246f6..1779c685b 100644
--- a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vmixMock.ts
+++ b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vmixMock.ts
@@ -157,9 +157,8 @@ function buildResponse(command: string, state?: 'OK' | 'ER', dataOrMessage?: str
// send every item in the array in a separate `data` event/packet
function sendData(socket: net.Socket, response: string[]) {
for (const packet of response) {
- const dataBuf = Buffer.from(packet, 'utf-8')
orgSetImmediate(() => {
- socket.mockData(dataBuf)
+ socket.mockData(packet)
})
}
}
diff --git a/packages/timeline-state-resolver/src/integrations/vmix/connection.ts b/packages/timeline-state-resolver/src/integrations/vmix/connection.ts
index 4efae0d50..aaa3e8fbf 100644
--- a/packages/timeline-state-resolver/src/integrations/vmix/connection.ts
+++ b/packages/timeline-state-resolver/src/integrations/vmix/connection.ts
@@ -114,6 +114,12 @@ export class BaseConnection extends EventEmitter {
this._socket.setEncoding('utf-8')
this._socket.on('data', (data) => {
+ if (typeof data !== 'string') {
+ // this is against the types, but according to the docs the data will be a string
+ // the problem of a character split into chunks in transit should be taken care of
+ // (https://nodejs.org/docs/latest-v12.x/api/stream.html#stream_readable_setencoding_encoding)
+ throw new Error('Received a non-string even though encoding should have been set to utf-8')
+ }
this._responseStreamReader.processIncomingData(data)
})
this._socket.on('connect', () => {
diff --git a/packages/timeline-state-resolver/src/integrations/vmix/vMixResponseStreamReader.ts b/packages/timeline-state-resolver/src/integrations/vmix/vMixResponseStreamReader.ts
index 7657f82b0..0f3fbfbaa 100644
--- a/packages/timeline-state-resolver/src/integrations/vmix/vMixResponseStreamReader.ts
+++ b/packages/timeline-state-resolver/src/integrations/vmix/vMixResponseStreamReader.ts
@@ -26,17 +26,18 @@ export class VMixResponseStreamReader extends EventEmitter