Skip to content

Commit 1b20006

Browse files
committed
refactor(frontend): replace emitData() with 'raw' structure type
Per PR review feedback, drop the parallel emitData() methods at every layer in favour of extending sendMessageToStreamer's structure vocabulary with 'raw'. Single entry point for to-streamer messages, binary payloads ride the same path as every other typed structure. Wire format unchanged; existing callers do not need to update.
1 parent e662029 commit 1b20006

6 files changed

Lines changed: 109 additions & 77 deletions

File tree

.changeset/data-channel-raw-bytes.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,11 @@
22
"@epicgames-ps/lib-pixelstreamingfrontend-ue5.7": minor
33
---
44

5-
Add `PixelStreaming.emitData(messageType, bytes)` for sending raw byte payloads through the data channel without JSON encoding (#608). The bytes are sent as the payload of a registered to-streamer message type — the registered id is prepended as a single byte, the rest of the buffer is the application's payload verbatim. Useful for custom binary protocols (e.g. UTF-8 strings, packed structs) where the receiving UE side decodes the payload itself. The lower-level `WebRtcPlayerController.emitData` and `SendMessageController.sendBytesToStreamer` are also exposed for advanced callers.
5+
Add `'raw'` to the structure vocabulary in to-streamer message types (#608). A field declared as `'raw'` accepts a `Uint8Array` and is written into the message buffer verbatim, enabling custom binary protocols (e.g. UTF-8 strings, packed structs) where the receiving UE side decodes the payload itself. Send via the existing `sendMessageToStreamer` path:
6+
7+
```ts
8+
streamMessageController.toStreamerMessages.set('MyBinaryMsg', { id: 137, structure: ['raw'] });
9+
sendMessageController.sendMessageToStreamer('MyBinaryMsg', [myUint8Array]);
10+
```
11+
12+
Existing structure types and call sites are unchanged.

Frontend/library/src/PixelStreaming/PixelStreaming.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -813,29 +813,6 @@ export class PixelStreaming {
813813
return true;
814814
}
815815

816-
/**
817-
* Send raw bytes to the UE application through the data channel without
818-
* any JSON encoding or per-field encoding. The bytes are sent as the
819-
* payload of a registered to-streamer message type — the message id
820-
* is prepended as a single byte.
821-
*
822-
* The message type must be registered via
823-
* `streamMessageController.toStreamerMessages.set(...)` so the id is
824-
* known. Useful for custom binary protocols where the receiving side
825-
* (UE) decodes the payload itself.
826-
*
827-
* @param messageType - Name of a registered to-streamer message type.
828-
* @param bytes - Payload to send, not including the message id byte.
829-
* @returns true if the bytes were submitted, false if rejected (video
830-
* not ready, message type not registered, or data channel not open).
831-
*/
832-
public emitData(messageType: string, bytes: Uint8Array | ArrayBuffer): boolean {
833-
if (!this._webRtcController.videoPlayer.isVideoReady()) {
834-
return false;
835-
}
836-
return this._webRtcController.emitData(messageType, bytes);
837-
}
838-
839816
/**
840817
* Send a console command to UE application. Only allowed if UE has signaled that it allows
841818
* console commands.
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { DataChannelSender } from '../DataChannel/DataChannelSender';
2+
import { SendMessageController } from './SendMessageController';
3+
import { StreamMessageController } from './StreamMessageController';
4+
5+
describe('SendMessageController', () => {
6+
let streamMessageController: StreamMessageController;
7+
let dataChannelSender: DataChannelSender;
8+
let sendData: jest.Mock;
9+
let controller: SendMessageController;
10+
11+
beforeEach(() => {
12+
streamMessageController = new StreamMessageController();
13+
sendData = jest.fn();
14+
dataChannelSender = {
15+
canSend: () => true,
16+
sendData
17+
} as unknown as DataChannelSender;
18+
controller = new SendMessageController(dataChannelSender, streamMessageController);
19+
});
20+
21+
describe("structure: ['raw']", () => {
22+
it('prepends the message id byte and copies the Uint8Array verbatim', () => {
23+
streamMessageController.toStreamerMessages.set('MyBinaryMsg', {
24+
id: 137,
25+
structure: ['raw']
26+
});
27+
28+
const payload = new Uint8Array([0x00, 0x01, 0xff, 0x42]);
29+
controller.sendMessageToStreamer('MyBinaryMsg', [payload]);
30+
31+
expect(sendData).toHaveBeenCalledTimes(1);
32+
const sent = new Uint8Array(sendData.mock.calls[0][0] as ArrayBuffer);
33+
expect(Array.from(sent)).toEqual([137, 0x00, 0x01, 0xff, 0x42]);
34+
});
35+
36+
it('handles an empty payload', () => {
37+
streamMessageController.toStreamerMessages.set('EmptyMsg', {
38+
id: 200,
39+
structure: ['raw']
40+
});
41+
42+
controller.sendMessageToStreamer('EmptyMsg', [new Uint8Array(0)]);
43+
44+
const sent = new Uint8Array(sendData.mock.calls[0][0] as ArrayBuffer);
45+
expect(Array.from(sent)).toEqual([200]);
46+
});
47+
48+
it('packs raw alongside other typed fields', () => {
49+
streamMessageController.toStreamerMessages.set('Mixed', {
50+
id: 50,
51+
structure: ['uint8', 'raw', 'uint8']
52+
});
53+
54+
controller.sendMessageToStreamer('Mixed', [
55+
0xaa,
56+
new Uint8Array([0xbb, 0xcc]),
57+
0xdd
58+
]);
59+
60+
const sent = new Uint8Array(sendData.mock.calls[0][0] as ArrayBuffer);
61+
expect(Array.from(sent)).toEqual([50, 0xaa, 0xbb, 0xcc, 0xdd]);
62+
});
63+
});
64+
65+
describe('backwards compatibility', () => {
66+
it('still packs typed structures as before', () => {
67+
streamMessageController.toStreamerMessages.set('Typed', {
68+
id: 1,
69+
structure: ['uint8', 'uint16']
70+
});
71+
72+
controller.sendMessageToStreamer('Typed', [0x10, 0x2030]);
73+
74+
const sent = new Uint8Array(sendData.mock.calls[0][0] as ArrayBuffer);
75+
// id (1), uint8 0x10, uint16 0x2030 little-endian = 0x30, 0x20
76+
expect(Array.from(sent)).toEqual([1, 0x10, 0x30, 0x20]);
77+
});
78+
});
79+
});

Frontend/library/src/UeInstanceMessage/SendMessageController.ts

Lines changed: 21 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class SendMessageController {
2626
* @param messageData - the message data we are sending over the data channel
2727
* @returns - nil
2828
*/
29-
sendMessageToStreamer(messageType: string, messageData?: Array<number | string>) {
29+
sendMessageToStreamer(messageType: string, messageData?: Array<number | string | Uint8Array>) {
3030
if (messageData === undefined) {
3131
messageData = [];
3232
}
@@ -54,16 +54,22 @@ export class SendMessageController {
5454
return 'number';
5555
case 'string':
5656
return 'string';
57+
case 'raw':
58+
return 'Uint8Array';
5759
}
5860
})
59-
.toString()} ] but received [ ${messageData.map((element: number | string) => typeof element).toString()} ]`
61+
.toString()} ] but received [ ${messageData
62+
.map((element: number | string | Uint8Array) =>
63+
element instanceof Uint8Array ? 'Uint8Array' : typeof element
64+
)
65+
.toString()} ]`
6066
);
6167
return;
6268
}
6369

6470
let byteLength = 0;
6571
// One loop to calculate the length in bytes of all of the provided data
66-
messageData.forEach((element: number | string, idx: number) => {
72+
messageData.forEach((element: number | string | Uint8Array, idx: number) => {
6773
const type = messageFormat.structure[idx];
6874
switch (type) {
6975
case 'uint8':
@@ -92,14 +98,18 @@ export class SendMessageController {
9298
// 2 bytes per character
9399
byteLength += 2 * (element as string).length;
94100
break;
101+
102+
case 'raw':
103+
byteLength += (element as Uint8Array).byteLength;
104+
break;
95105
}
96106
});
97107

98108
const data = new DataView(new ArrayBuffer(byteLength + 1));
99109
data.setUint8(0, messageFormat.id);
100110
let byteOffset = 1;
101111

102-
messageData.forEach((element: number | string, idx: number) => {
112+
messageData.forEach((element: number | string | Uint8Array, idx: number) => {
103113
const type = messageFormat.structure[idx];
104114
switch (type) {
105115
case 'uint8':
@@ -135,6 +145,13 @@ export class SendMessageController {
135145
byteOffset += 2;
136146
}
137147
break;
148+
149+
case 'raw': {
150+
const bytes = element as Uint8Array;
151+
new Uint8Array(data.buffer).set(bytes, byteOffset);
152+
byteOffset += bytes.byteLength;
153+
break;
154+
}
138155
}
139156
});
140157

@@ -149,44 +166,4 @@ export class SendMessageController {
149166

150167
this.dataChannelSender.sendData(data.buffer);
151168
}
152-
153-
/**
154-
* Send a raw byte payload for a registered to-streamer message type. The
155-
* registered message id is prepended as a single byte, then the bytes
156-
* are sent through the data channel as-is — no JSON encoding, no
157-
* per-field structure validation.
158-
*
159-
* Useful for custom protocols where the application owns the binary
160-
* payload format on both ends. The application must have called
161-
* `streamMessageController.toStreamerMessages.set(...)` (or relied on
162-
* a default-registered type such as `UIInteraction`) so the message id
163-
* is known.
164-
*
165-
* @param messageType - Name of a registered to-streamer message type.
166-
* @param bytes - Payload to send, not including the message id byte.
167-
* @returns true if the bytes were submitted to the data channel; false
168-
* if the message type isn't registered or the channel can't send.
169-
*/
170-
sendBytesToStreamer(messageType: string, bytes: Uint8Array | ArrayBuffer): boolean {
171-
const messageFormat = this.toStreamerMessagesMapProvider.toStreamerMessages.get(messageType);
172-
if (messageFormat === undefined) {
173-
Logger.Error(
174-
`Attempted to send raw bytes for message type "${messageType}" but no such type is registered. Register it via streamMessageController.toStreamerMessages.set(...)`
175-
);
176-
return false;
177-
}
178-
179-
if (!this.dataChannelSender.canSend()) {
180-
Logger.Info(`Data channel cannot send yet, skipping raw bytes for: ${messageType}`);
181-
return false;
182-
}
183-
184-
const payload = bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes;
185-
const buffer = new ArrayBuffer(1 + payload.byteLength);
186-
const view = new Uint8Array(buffer);
187-
view[0] = messageFormat.id;
188-
view.set(payload, 1);
189-
this.dataChannelSender.sendData(buffer);
190-
return true;
191-
}
192169
}

Frontend/library/src/UeInstanceMessage/StreamMessageController.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export class ToStreamerMessage {
88
}
99

1010
export class StreamMessageController {
11-
toStreamerHandlers: Map<string, (messageData?: Array<number | string>) => void>;
11+
toStreamerHandlers: Map<string, (messageData?: Array<number | string | Uint8Array>) => void>;
1212
fromStreamerHandlers: Map<string, (messageType: string, messageData?: ArrayBuffer) => void>;
1313
// Type Format
1414
toStreamerMessages: Map<string, ToStreamerMessage>;

Frontend/library/src/WebRtcPlayer/WebRtcPlayerController.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,14 +1827,6 @@ export class WebRtcPlayerController {
18271827
this.streamMessageController.toStreamerHandlers.get('Command')([JSON.stringify(descriptor)]);
18281828
}
18291829

1830-
/**
1831-
* Send raw bytes through the data channel for a registered to-streamer
1832-
* message type. See {@link SendMessageController.sendBytesToStreamer}.
1833-
*/
1834-
emitData(messageType: string, bytes: Uint8Array | ArrayBuffer): boolean {
1835-
return this.sendMessageController.sendBytesToStreamer(messageType, bytes);
1836-
}
1837-
18381830
/**
18391831
* Send a console command message
18401832
*/

0 commit comments

Comments
 (0)