livekit-client 2.18.9 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +5609 -644
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +3553 -2813
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.pt.worker.js +2 -0
- package/dist/livekit-client.pt.worker.js.map +1 -0
- package/dist/livekit-client.pt.worker.mjs +5834 -0
- package/dist/livekit-client.pt.worker.mjs.map +1 -0
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +2 -1
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +8 -7
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +35 -8
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +5 -5
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/DataCryptor.d.ts +5 -5
- package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +21 -4
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/sifPayload.d.ts +7 -7
- package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +7 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/packetTrailer/PacketTrailerManager.d.ts +49 -0
- package/dist/src/packetTrailer/PacketTrailerManager.d.ts.map +1 -0
- package/dist/src/packetTrailer/packetTrailer.d.ts +32 -0
- package/dist/src/packetTrailer/packetTrailer.d.ts.map +1 -0
- package/dist/src/packetTrailer/types.d.ts +57 -0
- package/dist/src/packetTrailer/types.d.ts.map +1 -0
- package/dist/src/packetTrailer/utils.d.ts +9 -0
- package/dist/src/packetTrailer/utils.d.ts.map +1 -0
- package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
- package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts.map +1 -0
- package/dist/src/room/RTCEngine.d.ts +2 -4
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +7 -3
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/RemoteDataTrack.d.ts +5 -1
- package/dist/src/room/data-track/RemoteDataTrack.d.ts.map +1 -1
- package/dist/src/room/data-track/depacketizer.d.ts +12 -4
- package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
- package/dist/src/room/data-track/frame.d.ts +3 -3
- package/dist/src/room/data-track/frame.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
- package/dist/src/room/data-track/incoming/IncomingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/incoming/pipeline.d.ts +4 -1
- package/dist/src/room/data-track/incoming/pipeline.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/types.d.ts +2 -2
- package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/extensions.d.ts +4 -4
- package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/index.d.ts +5 -5
- package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/serializable.d.ts +1 -1
- package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
- package/dist/src/room/data-track/types.d.ts +7 -0
- package/dist/src/room/data-track/types.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +2 -2
- package/dist/src/room/participant/LocalParticipant.d.ts +8 -14
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -1
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts +5 -1
- package/dist/src/room/participant/RemoteParticipant.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts +39 -0
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -0
- package/dist/src/room/rpc/client/events.d.ts +8 -0
- package/dist/src/room/rpc/client/events.d.ts.map +1 -0
- package/dist/src/room/rpc/index.d.ts +6 -0
- package/dist/src/room/rpc/index.d.ts.map +1 -0
- package/dist/src/room/rpc/server/RpcServerManager.d.ts +44 -0
- package/dist/src/room/rpc/server/RpcServerManager.d.ts.map +1 -0
- package/dist/src/room/rpc/server/events.d.ts +8 -0
- package/dist/src/room/rpc/server/events.d.ts.map +1 -0
- package/dist/src/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
- package/dist/src/room/rpc/utils.d.ts.map +1 -0
- package/dist/src/room/track/PacketTrailerExtractor.d.ts +19 -0
- package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +1 -0
- package/dist/src/room/track/RemoteVideoTrack.d.ts +16 -0
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -1
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/create.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +10 -0
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +4 -3
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/MockMediaStreamTrack.d.ts.map +1 -1
- package/dist/src/utils/dataPacketBuffer.d.ts +1 -1
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
- package/dist/src/version.d.ts +9 -1
- package/dist/src/version.d.ts.map +1 -1
- package/dist/ts4.2/api/SignalClient.d.ts +2 -1
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +8 -7
- package/dist/ts4.2/e2ee/types.d.ts +35 -8
- package/dist/ts4.2/e2ee/utils.d.ts +5 -5
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +5 -5
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +21 -4
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -1
- package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +7 -7
- package/dist/ts4.2/index.d.ts +5 -1
- package/dist/ts4.2/options.d.ts +7 -0
- package/dist/ts4.2/packetTrailer/PacketTrailerManager.d.ts +49 -0
- package/dist/ts4.2/packetTrailer/packetTrailer.d.ts +32 -0
- package/dist/ts4.2/packetTrailer/types.d.ts +57 -0
- package/dist/ts4.2/packetTrailer/utils.d.ts +9 -0
- package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +2 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +2 -4
- package/dist/ts4.2/room/Room.d.ts +7 -3
- package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +5 -1
- package/dist/ts4.2/room/data-track/depacketizer.d.ts +12 -4
- package/dist/ts4.2/room/data-track/frame.d.ts +3 -3
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +3 -1
- package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +4 -1
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +2 -2
- package/dist/ts4.2/room/data-track/packet/extensions.d.ts +4 -4
- package/dist/ts4.2/room/data-track/packet/index.d.ts +5 -5
- package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -1
- package/dist/ts4.2/room/data-track/types.d.ts +7 -0
- package/dist/ts4.2/room/events.d.ts +2 -2
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +8 -14
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +5 -1
- package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -0
- package/dist/ts4.2/room/rpc/client/events.d.ts +8 -0
- package/dist/ts4.2/room/rpc/index.d.ts +7 -0
- package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -0
- package/dist/ts4.2/room/rpc/server/events.d.ts +8 -0
- package/dist/ts4.2/room/{rpc.d.ts → rpc/utils.d.ts} +34 -4
- package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +19 -0
- package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +16 -0
- package/dist/ts4.2/room/track/Track.d.ts +1 -1
- package/dist/ts4.2/room/track/options.d.ts +10 -0
- package/dist/ts4.2/room/utils.d.ts +4 -3
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +1 -1
- package/dist/ts4.2/version.d.ts +9 -1
- package/package.json +24 -16
- package/src/api/SignalClient.test.ts +102 -10
- package/src/api/SignalClient.ts +4 -2
- package/src/api/WebSocketStream.test.ts +0 -1
- package/src/e2ee/E2eeManager.ts +82 -30
- package/src/e2ee/types.ts +37 -8
- package/src/e2ee/utils.ts +7 -6
- package/src/e2ee/worker/DataCryptor.ts +6 -6
- package/src/e2ee/worker/FrameCryptor.test.ts +177 -4
- package/src/e2ee/worker/FrameCryptor.ts +94 -14
- package/src/e2ee/worker/ParticipantKeyHandler.test.ts +4 -4
- package/src/e2ee/worker/e2ee.worker.ts +13 -5
- package/src/e2ee/worker/naluUtils.ts +4 -4
- package/src/e2ee/worker/sifPayload.ts +10 -8
- package/src/index.ts +7 -0
- package/src/options.ts +8 -0
- package/src/packetTrailer/PacketTrailerManager.test.ts +172 -0
- package/src/packetTrailer/PacketTrailerManager.ts +250 -0
- package/src/packetTrailer/packetTrailer.test.ts +174 -0
- package/src/packetTrailer/packetTrailer.ts +276 -0
- package/src/packetTrailer/types.ts +75 -0
- package/src/packetTrailer/utils.test.ts +105 -0
- package/src/packetTrailer/utils.ts +50 -0
- package/src/packetTrailer/worker/packetTrailer.worker.ts +155 -0
- package/src/packetTrailer/worker/tsconfig.json +14 -0
- package/src/room/BackOffStrategy.test.ts +1 -1
- package/src/room/RTCEngine.test.ts +219 -0
- package/src/room/RTCEngine.ts +86 -46
- package/src/room/Room.test.ts +62 -1
- package/src/room/Room.ts +111 -86
- package/src/room/data-track/RemoteDataTrack.ts +8 -1
- package/src/room/data-track/depacketizer.test.ts +433 -1
- package/src/room/data-track/depacketizer.ts +79 -61
- package/src/room/data-track/frame.ts +2 -2
- package/src/room/data-track/incoming/IncomingDataTrackManager.test.ts +194 -0
- package/src/room/data-track/incoming/IncomingDataTrackManager.ts +21 -1
- package/src/room/data-track/incoming/pipeline.ts +13 -2
- package/src/room/data-track/outgoing/types.ts +3 -2
- package/src/room/data-track/packet/extensions.ts +2 -2
- package/src/room/data-track/packet/index.ts +6 -6
- package/src/room/data-track/packet/serializable.ts +1 -1
- package/src/room/data-track/types.ts +8 -0
- package/src/room/events.ts +2 -2
- package/src/room/participant/LocalParticipant.test.ts +81 -0
- package/src/room/participant/LocalParticipant.ts +64 -187
- package/src/room/participant/Participant.ts +1 -1
- package/src/room/participant/RemoteParticipant.ts +9 -0
- package/src/room/participant/publishUtils.ts +1 -1
- package/src/room/rpc/client/RpcClientManager.test.ts +430 -0
- package/src/room/rpc/client/RpcClientManager.ts +269 -0
- package/src/room/rpc/client/events.ts +9 -0
- package/src/room/rpc/index.ts +14 -0
- package/src/room/rpc/server/RpcServerManager.test.ts +471 -0
- package/src/room/rpc/server/RpcServerManager.ts +293 -0
- package/src/room/rpc/server/events.ts +9 -0
- package/src/room/{rpc.ts → rpc/utils.ts} +49 -8
- package/src/room/track/PacketTrailerExtractor.ts +43 -0
- package/src/room/track/RemoteVideoTrack.ts +23 -2
- package/src/room/track/Track.ts +1 -1
- package/src/room/track/create.ts +0 -4
- package/src/room/track/options.ts +11 -0
- package/src/room/track/record.ts +1 -1
- package/src/room/track/utils.ts +4 -1
- package/src/room/utils.test.ts +14 -1
- package/src/room/utils.ts +19 -4
- package/src/test/MockMediaStreamTrack.ts +0 -1
- package/src/type-polyfills/non-shared-typed-arrays.d.ts +6 -0
- package/src/utils/dataPacketBuffer.ts +1 -1
- package/src/version.ts +11 -1
- package/dist/src/room/rpc.d.ts.map +0 -1
- package/src/room/rpc.test.ts +0 -301
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
appendPacketTrailer,
|
|
4
|
+
appendPacketTrailerToEncodedFrame,
|
|
5
|
+
extractPacketTrailer,
|
|
6
|
+
processPacketTrailer,
|
|
7
|
+
} from './packetTrailer';
|
|
8
|
+
|
|
9
|
+
describe('packetTrailer', () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
vi.useRealTimers();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('extracts user timestamp and frame id from packet trailer', () => {
|
|
15
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
16
|
+
const trailer = appendPacketTrailer(payload, 1_744_249_600_123_456n, 42);
|
|
17
|
+
const extracted = extractPacketTrailer(trailer);
|
|
18
|
+
|
|
19
|
+
expect(Array.from(extracted.data)).toEqual(Array.from(payload));
|
|
20
|
+
expect(extracted.metadata).toEqual({
|
|
21
|
+
userTimestamp: 1_744_249_600_123_456n,
|
|
22
|
+
frameId: 42,
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('extracts timestamp-only trailer when frameId is 0', () => {
|
|
27
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
28
|
+
const trailer = appendPacketTrailer(payload, 1_744_249_600_123_456n, 0);
|
|
29
|
+
const extracted = extractPacketTrailer(trailer);
|
|
30
|
+
|
|
31
|
+
expect(Array.from(extracted.data)).toEqual(Array.from(payload));
|
|
32
|
+
expect(extracted.metadata).toEqual({
|
|
33
|
+
userTimestamp: 1_744_249_600_123_456n,
|
|
34
|
+
frameId: 0,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('extracts frameId-only trailer when timestamp is 0', () => {
|
|
39
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
40
|
+
const trailer = appendPacketTrailer(payload, 0n, 42);
|
|
41
|
+
const extracted = extractPacketTrailer(trailer);
|
|
42
|
+
|
|
43
|
+
expect(Array.from(extracted.data)).toEqual(Array.from(payload));
|
|
44
|
+
expect(extracted.metadata).toEqual({
|
|
45
|
+
userTimestamp: 0n,
|
|
46
|
+
frameId: 42,
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('returns data unchanged when both timestamp and frameId are 0', () => {
|
|
51
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
52
|
+
const result = appendPacketTrailer(payload, 0n, 0);
|
|
53
|
+
|
|
54
|
+
expect(Array.from(result)).toEqual(Array.from(payload));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('passes frames through when there is no valid trailer', () => {
|
|
58
|
+
const payload = Uint8Array.from([1, 2, 3, 4, 5]);
|
|
59
|
+
const extracted = extractPacketTrailer(payload);
|
|
60
|
+
|
|
61
|
+
expect(Array.from(extracted.data)).toEqual(Array.from(payload));
|
|
62
|
+
expect(extracted.metadata).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('uses the encoded frame timestamp when metadata does not include an RTP timestamp', () => {
|
|
66
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
67
|
+
const trailer = appendPacketTrailer(payload, 1_744_249_600_123_456n, 42);
|
|
68
|
+
const frame = {
|
|
69
|
+
data: trailer.buffer,
|
|
70
|
+
timestamp: 1234,
|
|
71
|
+
getMetadata() {
|
|
72
|
+
return {};
|
|
73
|
+
},
|
|
74
|
+
} as unknown as RTCEncodedVideoFrame;
|
|
75
|
+
|
|
76
|
+
const result = processPacketTrailer(frame, 'track-id');
|
|
77
|
+
|
|
78
|
+
expect(result.payload).toEqual({
|
|
79
|
+
trackId: 'track-id',
|
|
80
|
+
rtpTimestamp: 1234,
|
|
81
|
+
ssrc: 0,
|
|
82
|
+
metadata: {
|
|
83
|
+
userTimestamp: 1_744_249_600_123_456n,
|
|
84
|
+
frameId: 42,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('appends timestamp-only packet trailer to encoded frames', () => {
|
|
90
|
+
vi.useFakeTimers();
|
|
91
|
+
vi.setSystemTime(new Date('2025-04-10T12:00:00.123Z'));
|
|
92
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
93
|
+
const frame = { data: payload.buffer } as RTCEncodedVideoFrame;
|
|
94
|
+
|
|
95
|
+
appendPacketTrailerToEncodedFrame(frame, { timestamp: true }, 0);
|
|
96
|
+
const extracted = extractPacketTrailer(frame.data);
|
|
97
|
+
|
|
98
|
+
expect(Array.from(extracted.data)).toEqual(Array.from(payload));
|
|
99
|
+
expect(frame.data.byteLength).toBe(payload.byteLength + 15);
|
|
100
|
+
expect(extracted.metadata).toEqual({
|
|
101
|
+
userTimestamp: BigInt(Date.now()) * BigInt(1000),
|
|
102
|
+
frameId: 0,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('appends frame-id-only packet trailer to encoded frames', () => {
|
|
107
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
108
|
+
const firstFrame = { data: payload.buffer.slice(0) } as RTCEncodedVideoFrame;
|
|
109
|
+
const secondFrame = { data: payload.buffer.slice(0) } as RTCEncodedVideoFrame;
|
|
110
|
+
|
|
111
|
+
appendPacketTrailerToEncodedFrame(firstFrame, { frameId: true }, 1);
|
|
112
|
+
appendPacketTrailerToEncodedFrame(secondFrame, { frameId: true }, 2);
|
|
113
|
+
|
|
114
|
+
expect(firstFrame.data.byteLength).toBe(payload.byteLength + 11);
|
|
115
|
+
expect(secondFrame.data.byteLength).toBe(payload.byteLength + 11);
|
|
116
|
+
expect(extractPacketTrailer(firstFrame.data).metadata).toEqual({
|
|
117
|
+
userTimestamp: 0n,
|
|
118
|
+
frameId: 1,
|
|
119
|
+
});
|
|
120
|
+
expect(extractPacketTrailer(secondFrame.data).metadata).toEqual({
|
|
121
|
+
userTimestamp: 0n,
|
|
122
|
+
frameId: 2,
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('appends both timestamp and frame id to encoded frames', () => {
|
|
127
|
+
vi.useFakeTimers();
|
|
128
|
+
vi.setSystemTime(new Date('2025-04-10T12:00:00.123Z'));
|
|
129
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
130
|
+
const frame = { data: payload.buffer } as RTCEncodedVideoFrame;
|
|
131
|
+
|
|
132
|
+
appendPacketTrailerToEncodedFrame(frame, { timestamp: true, frameId: true }, 7);
|
|
133
|
+
|
|
134
|
+
expect(extractPacketTrailer(frame.data).metadata).toEqual({
|
|
135
|
+
userTimestamp: BigInt(Date.now()) * BigInt(1000),
|
|
136
|
+
frameId: 7,
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it.each([{}, { timestamp: false, frameId: false }])(
|
|
141
|
+
'passes encoded frames through when no write features are enabled: %o',
|
|
142
|
+
(packetTrailer) => {
|
|
143
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
144
|
+
const frame = { data: payload.buffer } as RTCEncodedVideoFrame;
|
|
145
|
+
|
|
146
|
+
const changed = appendPacketTrailerToEncodedFrame(frame, packetTrailer, 1);
|
|
147
|
+
|
|
148
|
+
expect(changed).toBe(false);
|
|
149
|
+
expect(frame.data).toBe(payload.buffer);
|
|
150
|
+
expect(extractPacketTrailer(frame.data).metadata).toBeUndefined();
|
|
151
|
+
},
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
it('passes encoded frames through when publish options omit enabled features', () => {
|
|
155
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
156
|
+
const frame = { data: payload.buffer } as RTCEncodedVideoFrame;
|
|
157
|
+
|
|
158
|
+
const changed = appendPacketTrailerToEncodedFrame(frame, { timestamp: false }, 1);
|
|
159
|
+
|
|
160
|
+
expect(changed).toBe(false);
|
|
161
|
+
expect(frame.data).toBe(payload.buffer);
|
|
162
|
+
expect(extractPacketTrailer(frame.data).metadata).toBeUndefined();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('passes encoded frames through when publish options are undefined', () => {
|
|
166
|
+
const payload = Uint8Array.from([1, 2, 3, 4]);
|
|
167
|
+
const frame = { data: payload.buffer } as RTCEncodedVideoFrame;
|
|
168
|
+
|
|
169
|
+
const changed = appendPacketTrailerToEncodedFrame(frame, undefined, 1);
|
|
170
|
+
|
|
171
|
+
expect(changed).toBe(false);
|
|
172
|
+
expect(frame.data).toBe(payload.buffer);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import type { PacketTrailerMetadata, PacketTrailerPublishOptions } from './types';
|
|
2
|
+
import { hasPacketTrailerPublishOptions } from './utils';
|
|
3
|
+
|
|
4
|
+
export const PACKET_TRAILER_MAGIC = Uint8Array.from([
|
|
5
|
+
'L'.charCodeAt(0),
|
|
6
|
+
'K'.charCodeAt(0),
|
|
7
|
+
'T'.charCodeAt(0),
|
|
8
|
+
'S'.charCodeAt(0),
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
export const PACKET_TRAILER_TIMESTAMP_TAG = 0x01;
|
|
12
|
+
export const PACKET_TRAILER_FRAME_ID_TAG = 0x02;
|
|
13
|
+
export const PACKET_TRAILER_ENVELOPE_SIZE = 5;
|
|
14
|
+
|
|
15
|
+
const TIMESTAMP_TLV_SIZE = 10;
|
|
16
|
+
const FRAME_ID_TLV_SIZE = 6;
|
|
17
|
+
|
|
18
|
+
export interface ExtractPacketTrailerResult {
|
|
19
|
+
data: Uint8Array;
|
|
20
|
+
metadata?: PacketTrailerMetadata;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function appendPacketTrailer(
|
|
24
|
+
data: Uint8Array,
|
|
25
|
+
userTimestamp: bigint,
|
|
26
|
+
frameId: number,
|
|
27
|
+
): Uint8Array {
|
|
28
|
+
const hasTimestamp = userTimestamp !== BigInt(0);
|
|
29
|
+
const hasFrameId = frameId !== 0;
|
|
30
|
+
|
|
31
|
+
if (!hasTimestamp && !hasFrameId) {
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const trailerLength =
|
|
36
|
+
(hasTimestamp ? TIMESTAMP_TLV_SIZE : 0) +
|
|
37
|
+
(hasFrameId ? FRAME_ID_TLV_SIZE : 0) +
|
|
38
|
+
PACKET_TRAILER_ENVELOPE_SIZE;
|
|
39
|
+
const result = new Uint8Array(data.length + trailerLength);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
|
|
42
|
+
result.set(data, offset);
|
|
43
|
+
offset += data.length;
|
|
44
|
+
|
|
45
|
+
if (hasTimestamp) {
|
|
46
|
+
result[offset++] = PACKET_TRAILER_TIMESTAMP_TAG ^ 0xff;
|
|
47
|
+
result[offset++] = 8 ^ 0xff;
|
|
48
|
+
writeUint64Xor(result, offset, userTimestamp);
|
|
49
|
+
offset += 8;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (hasFrameId) {
|
|
53
|
+
result[offset++] = PACKET_TRAILER_FRAME_ID_TAG ^ 0xff;
|
|
54
|
+
result[offset++] = 4 ^ 0xff;
|
|
55
|
+
writeUint32Xor(result, offset, frameId);
|
|
56
|
+
offset += 4;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
result[offset++] = trailerLength ^ 0xff;
|
|
60
|
+
result.set(PACKET_TRAILER_MAGIC, offset);
|
|
61
|
+
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function appendPacketTrailerToEncodedFrame(
|
|
66
|
+
frame: RTCEncodedVideoFrame,
|
|
67
|
+
options: PacketTrailerPublishOptions | undefined,
|
|
68
|
+
frameId: number,
|
|
69
|
+
): boolean {
|
|
70
|
+
if (!hasPacketTrailerPublishOptions(options) || frame.data.byteLength === 0) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const userTimestamp = options?.timestamp ? BigInt(Date.now()) * BigInt(1000) : BigInt(0);
|
|
75
|
+
const packetTrailerFrameId = options?.frameId ? frameId : 0;
|
|
76
|
+
const data = new Uint8Array(frame.data);
|
|
77
|
+
const result = appendPacketTrailer(data, userTimestamp, packetTrailerFrameId);
|
|
78
|
+
|
|
79
|
+
if (result.byteLength === data.byteLength) {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
frame.data = result.buffer.slice(
|
|
84
|
+
result.byteOffset,
|
|
85
|
+
result.byteOffset + result.byteLength,
|
|
86
|
+
) as ArrayBuffer;
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function extractPacketTrailer(data: ArrayBuffer | Uint8Array): ExtractPacketTrailerResult {
|
|
91
|
+
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
92
|
+
if (bytes.length < PACKET_TRAILER_ENVELOPE_SIZE) {
|
|
93
|
+
return { data: bytes };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const magicOffset = bytes.length - PACKET_TRAILER_MAGIC.length;
|
|
97
|
+
if (!matchesMagic(bytes, magicOffset)) {
|
|
98
|
+
return { data: bytes };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const trailerLength = bytes[bytes.length - PACKET_TRAILER_ENVELOPE_SIZE] ^ 0xff;
|
|
102
|
+
if (trailerLength < PACKET_TRAILER_ENVELOPE_SIZE || trailerLength > bytes.length) {
|
|
103
|
+
return { data: bytes };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const trailerStart = bytes.length - trailerLength;
|
|
107
|
+
const trailerEnd = bytes.length - PACKET_TRAILER_ENVELOPE_SIZE;
|
|
108
|
+
const strippedData = bytes.subarray(0, trailerStart);
|
|
109
|
+
let offset = trailerStart;
|
|
110
|
+
let foundAny = false;
|
|
111
|
+
const metadata: PacketTrailerMetadata = {
|
|
112
|
+
userTimestamp: BigInt(0),
|
|
113
|
+
frameId: 0,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
while (offset + 2 <= trailerEnd) {
|
|
117
|
+
const tag = bytes[offset++] ^ 0xff;
|
|
118
|
+
const length = bytes[offset++] ^ 0xff;
|
|
119
|
+
|
|
120
|
+
if (offset + length > trailerEnd) {
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (tag === PACKET_TRAILER_TIMESTAMP_TAG && length === 8) {
|
|
125
|
+
metadata.userTimestamp = readUint64Xor(bytes, offset);
|
|
126
|
+
foundAny = true;
|
|
127
|
+
} else if (tag === PACKET_TRAILER_FRAME_ID_TAG && length === 4) {
|
|
128
|
+
metadata.frameId = readUint32Xor(bytes, offset, length);
|
|
129
|
+
foundAny = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
offset += length;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!foundAny) {
|
|
136
|
+
return { data: bytes };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { data: strippedData, metadata };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function matchesMagic(data: Uint8Array, offset: number) {
|
|
143
|
+
for (let index = 0; index < PACKET_TRAILER_MAGIC.length; index += 1) {
|
|
144
|
+
if (data[offset + index] !== PACKET_TRAILER_MAGIC[index]) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function readUint64Xor(data: Uint8Array, offset: number): bigint {
|
|
152
|
+
const hi = BigInt(
|
|
153
|
+
(((data[offset] ^ 0xff) << 24) |
|
|
154
|
+
((data[offset + 1] ^ 0xff) << 16) |
|
|
155
|
+
((data[offset + 2] ^ 0xff) << 8) |
|
|
156
|
+
(data[offset + 3] ^ 0xff)) >>>
|
|
157
|
+
0,
|
|
158
|
+
);
|
|
159
|
+
const lo = BigInt(
|
|
160
|
+
(((data[offset + 4] ^ 0xff) << 24) |
|
|
161
|
+
((data[offset + 5] ^ 0xff) << 16) |
|
|
162
|
+
((data[offset + 6] ^ 0xff) << 8) |
|
|
163
|
+
(data[offset + 7] ^ 0xff)) >>>
|
|
164
|
+
0,
|
|
165
|
+
);
|
|
166
|
+
return (hi << BigInt(32)) | lo;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function readUint32Xor(data: Uint8Array, offset: number, length: number) {
|
|
170
|
+
let value = 0;
|
|
171
|
+
for (let index = 0; index < length; index += 1) {
|
|
172
|
+
value = (value << 8) | (data[offset + index] ^ 0xff);
|
|
173
|
+
}
|
|
174
|
+
return value >>> 0;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function writeUint64Xor(target: Uint8Array, offset: number, value: bigint) {
|
|
178
|
+
const hi = Number((value >> BigInt(32)) & BigInt(0xffffffff));
|
|
179
|
+
const lo = Number(value & BigInt(0xffffffff));
|
|
180
|
+
target[offset] = (hi >>> 24) ^ 0xff;
|
|
181
|
+
target[offset + 1] = ((hi >>> 16) & 0xff) ^ 0xff;
|
|
182
|
+
target[offset + 2] = ((hi >>> 8) & 0xff) ^ 0xff;
|
|
183
|
+
target[offset + 3] = (hi & 0xff) ^ 0xff;
|
|
184
|
+
target[offset + 4] = (lo >>> 24) ^ 0xff;
|
|
185
|
+
target[offset + 5] = ((lo >>> 16) & 0xff) ^ 0xff;
|
|
186
|
+
target[offset + 6] = ((lo >>> 8) & 0xff) ^ 0xff;
|
|
187
|
+
target[offset + 7] = (lo & 0xff) ^ 0xff;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function writeUint32Xor(target: Uint8Array, offset: number, value: number) {
|
|
191
|
+
for (let index = 3; index >= 0; index -= 1) {
|
|
192
|
+
target[offset + (3 - index)] = ((value >> (index * 8)) & 0xff) ^ 0xff;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function getFrameRtpTimestamp(
|
|
197
|
+
frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
|
|
198
|
+
): number | undefined {
|
|
199
|
+
try {
|
|
200
|
+
const metadata = frame.getMetadata() as Record<string, unknown>;
|
|
201
|
+
if (typeof metadata.rtpTimestamp === 'number') {
|
|
202
|
+
return metadata.rtpTimestamp;
|
|
203
|
+
}
|
|
204
|
+
if (typeof metadata.timestamp === 'number') {
|
|
205
|
+
return metadata.timestamp;
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// getMetadata() might not be available
|
|
209
|
+
}
|
|
210
|
+
if (typeof frame.timestamp === 'number') {
|
|
211
|
+
return frame.timestamp;
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function getFrameSsrc(frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame): number {
|
|
217
|
+
try {
|
|
218
|
+
const metadata = frame.getMetadata() as Record<string, unknown>;
|
|
219
|
+
if (typeof metadata.synchronizationSource === 'number') {
|
|
220
|
+
return metadata.synchronizationSource;
|
|
221
|
+
}
|
|
222
|
+
} catch {}
|
|
223
|
+
return 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export interface PacketTrailerFramePayload {
|
|
227
|
+
trackId: string;
|
|
228
|
+
rtpTimestamp: number;
|
|
229
|
+
ssrc: number;
|
|
230
|
+
metadata: PacketTrailerMetadata;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface ProcessPacketTrailerResult {
|
|
234
|
+
data?: ArrayBuffer;
|
|
235
|
+
payload?: PacketTrailerFramePayload;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Extracts a packet trailer from an encoded frame and returns the stripped
|
|
240
|
+
* frame data (if any) along with a ready-to-post metadata payload. Returns an
|
|
241
|
+
* empty object when no trailer is present, an RTP timestamp can't be read, or
|
|
242
|
+
* a trackId isn't available.
|
|
243
|
+
*/
|
|
244
|
+
export function processPacketTrailer(
|
|
245
|
+
frame: RTCEncodedVideoFrame | RTCEncodedAudioFrame,
|
|
246
|
+
trackId: string | undefined,
|
|
247
|
+
): ProcessPacketTrailerResult {
|
|
248
|
+
if (frame.data.byteLength === 0) {
|
|
249
|
+
return {};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const result = extractPacketTrailer(frame.data);
|
|
253
|
+
if (!result.metadata) {
|
|
254
|
+
return {};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const strippedData = (result.data.buffer as ArrayBuffer).slice(
|
|
258
|
+
result.data.byteOffset,
|
|
259
|
+
result.data.byteOffset + result.data.byteLength,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const rtpTimestamp = getFrameRtpTimestamp(frame);
|
|
263
|
+
if (rtpTimestamp === undefined || !trackId) {
|
|
264
|
+
return { data: strippedData };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
data: strippedData,
|
|
269
|
+
payload: {
|
|
270
|
+
trackId,
|
|
271
|
+
rtpTimestamp,
|
|
272
|
+
ssrc: getFrameSsrc(frame),
|
|
273
|
+
metadata: result.metadata,
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { PacketTrailerFramePayload } from './packetTrailer';
|
|
2
|
+
|
|
3
|
+
export interface PacketTrailerMetadata {
|
|
4
|
+
userTimestamp: bigint;
|
|
5
|
+
frameId: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface PacketTrailerPublishOptions {
|
|
9
|
+
timestamp?: boolean;
|
|
10
|
+
frameId?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PTBaseMessage {
|
|
14
|
+
kind: string;
|
|
15
|
+
data?: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PTInitMessage extends PTBaseMessage {
|
|
19
|
+
kind: 'init';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PTInitAck extends PTBaseMessage {
|
|
23
|
+
kind: 'initAck';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PTDecodeMessage extends PTBaseMessage {
|
|
27
|
+
kind: 'decode';
|
|
28
|
+
data: {
|
|
29
|
+
readableStream: ReadableStream;
|
|
30
|
+
writableStream: WritableStream;
|
|
31
|
+
trackId: string;
|
|
32
|
+
hasPacketTrailer: boolean;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PTEncodeMessage extends PTBaseMessage {
|
|
37
|
+
kind: 'encode';
|
|
38
|
+
data: {
|
|
39
|
+
readableStream: ReadableStream;
|
|
40
|
+
writableStream: WritableStream;
|
|
41
|
+
packetTrailer?: PacketTrailerPublishOptions;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type PTScriptTransformOptions =
|
|
46
|
+
| {
|
|
47
|
+
kind: 'decode';
|
|
48
|
+
trackId: string;
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
kind: 'encode';
|
|
52
|
+
packetTrailer?: PacketTrailerPublishOptions;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export interface PTMetadataMessage extends PTBaseMessage {
|
|
56
|
+
kind: 'metadata';
|
|
57
|
+
data: PacketTrailerFramePayload;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface PTUpdateTrackIdMessage extends PTBaseMessage {
|
|
61
|
+
kind: 'updateTrackId';
|
|
62
|
+
data: {
|
|
63
|
+
oldTrackId: string;
|
|
64
|
+
newTrackId: string;
|
|
65
|
+
hasPacketTrailer: boolean;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type PTWorkerMessage =
|
|
70
|
+
| PTInitMessage
|
|
71
|
+
| PTInitAck
|
|
72
|
+
| PTDecodeMessage
|
|
73
|
+
| PTEncodeMessage
|
|
74
|
+
| PTUpdateTrackIdMessage
|
|
75
|
+
| PTMetadataMessage;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { PacketTrailerFeature } from '@livekit/protocol';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import {
|
|
4
|
+
getPacketTrailerFeatures,
|
|
5
|
+
getPacketTrailerPublishOptions,
|
|
6
|
+
isPacketTrailerSupported,
|
|
7
|
+
} from './utils';
|
|
8
|
+
|
|
9
|
+
describe('packet trailer support', () => {
|
|
10
|
+
const originalRTCRtpSender = window.RTCRtpSender;
|
|
11
|
+
const originalRTCRtpScriptTransform = (window as unknown as { RTCRtpScriptTransform?: unknown })
|
|
12
|
+
.RTCRtpScriptTransform;
|
|
13
|
+
const originalUserAgent = navigator.userAgent;
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
Object.defineProperty(window, 'RTCRtpSender', {
|
|
17
|
+
configurable: true,
|
|
18
|
+
value: originalRTCRtpSender,
|
|
19
|
+
writable: true,
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(window, 'RTCRtpScriptTransform', {
|
|
22
|
+
configurable: true,
|
|
23
|
+
value: originalRTCRtpScriptTransform,
|
|
24
|
+
writable: true,
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(window.navigator, 'userAgent', {
|
|
27
|
+
configurable: true,
|
|
28
|
+
value: originalUserAgent,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
function stubScriptTransformSupport(userAgent: string) {
|
|
33
|
+
Object.defineProperty(window, 'RTCRtpSender', {
|
|
34
|
+
configurable: true,
|
|
35
|
+
value: undefined,
|
|
36
|
+
writable: true,
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(window, 'RTCRtpScriptTransform', {
|
|
39
|
+
configurable: true,
|
|
40
|
+
value: class MockRTCRtpScriptTransform {},
|
|
41
|
+
writable: true,
|
|
42
|
+
});
|
|
43
|
+
Object.defineProperty(window.navigator, 'userAgent', {
|
|
44
|
+
configurable: true,
|
|
45
|
+
value: userAgent,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
it('supports packet trailers with RTCRtpScriptTransform on Safari', () => {
|
|
50
|
+
stubScriptTransformSupport(
|
|
51
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
expect(isPacketTrailerSupported({ worker: {} as Worker })).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('supports packet trailers with RTCRtpScriptTransform on Firefox', () => {
|
|
58
|
+
stubScriptTransformSupport(
|
|
59
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14.0; rv:144.0) Gecko/20100101 Firefox/144.0',
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
expect(isPacketTrailerSupported({ worker: {} as Worker })).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('does not use RTCRtpScriptTransform support on Chromium-based browsers', () => {
|
|
66
|
+
stubScriptTransformSupport(
|
|
67
|
+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36',
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(isPacketTrailerSupported({ worker: {} as Worker })).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('packet trailer publish features', () => {
|
|
75
|
+
it('maps publish options to protocol features', () => {
|
|
76
|
+
expect(getPacketTrailerFeatures({ timestamp: true, frameId: true })).toEqual([
|
|
77
|
+
PacketTrailerFeature.PTF_USER_TIMESTAMP,
|
|
78
|
+
PacketTrailerFeature.PTF_FRAME_ID,
|
|
79
|
+
]);
|
|
80
|
+
expect(getPacketTrailerFeatures({ timestamp: true })).toEqual([
|
|
81
|
+
PacketTrailerFeature.PTF_USER_TIMESTAMP,
|
|
82
|
+
]);
|
|
83
|
+
expect(getPacketTrailerFeatures({ frameId: true })).toEqual([
|
|
84
|
+
PacketTrailerFeature.PTF_FRAME_ID,
|
|
85
|
+
]);
|
|
86
|
+
expect(getPacketTrailerFeatures()).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('maps protocol features to publish options', () => {
|
|
90
|
+
expect(
|
|
91
|
+
getPacketTrailerPublishOptions([
|
|
92
|
+
PacketTrailerFeature.PTF_USER_TIMESTAMP,
|
|
93
|
+
PacketTrailerFeature.PTF_FRAME_ID,
|
|
94
|
+
]),
|
|
95
|
+
).toEqual({ timestamp: true, frameId: true });
|
|
96
|
+
expect(getPacketTrailerPublishOptions([PacketTrailerFeature.PTF_USER_TIMESTAMP])).toEqual({
|
|
97
|
+
timestamp: true,
|
|
98
|
+
});
|
|
99
|
+
expect(getPacketTrailerPublishOptions([PacketTrailerFeature.PTF_FRAME_ID])).toEqual({
|
|
100
|
+
frameId: true,
|
|
101
|
+
});
|
|
102
|
+
expect(getPacketTrailerPublishOptions()).toBeUndefined();
|
|
103
|
+
expect(getPacketTrailerPublishOptions([])).toBeUndefined();
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { PacketTrailerFeature } from '@livekit/protocol';
|
|
2
|
+
import { isInsertableStreamSupported } from '../e2ee/utils';
|
|
3
|
+
import { isScriptTransformSupportedForWorker } from '../room/utils';
|
|
4
|
+
import type { PacketTrailerOptions } from './PacketTrailerManager';
|
|
5
|
+
import type { PacketTrailerPublishOptions } from './types';
|
|
6
|
+
|
|
7
|
+
export function shouldUsePacketTrailerScriptTransform() {
|
|
8
|
+
return isScriptTransformSupportedForWorker();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isPacketTrailerSupported(options?: PacketTrailerOptions) {
|
|
12
|
+
return (
|
|
13
|
+
!!options?.worker && (isInsertableStreamSupported() || shouldUsePacketTrailerScriptTransform())
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hasPacketTrailerPublishOptions(options?: PacketTrailerPublishOptions): boolean {
|
|
18
|
+
return !!(options?.timestamp || options?.frameId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getPacketTrailerFeatures(
|
|
22
|
+
options?: PacketTrailerPublishOptions,
|
|
23
|
+
): PacketTrailerFeature[] {
|
|
24
|
+
const features: PacketTrailerFeature[] = [];
|
|
25
|
+
if (options?.timestamp) {
|
|
26
|
+
features.push(PacketTrailerFeature.PTF_USER_TIMESTAMP);
|
|
27
|
+
}
|
|
28
|
+
if (options?.frameId) {
|
|
29
|
+
features.push(PacketTrailerFeature.PTF_FRAME_ID);
|
|
30
|
+
}
|
|
31
|
+
return features;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getPacketTrailerPublishOptions(
|
|
35
|
+
features?: PacketTrailerFeature[],
|
|
36
|
+
): PacketTrailerPublishOptions | undefined {
|
|
37
|
+
if (!features || features.length === 0) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const options: PacketTrailerPublishOptions = {};
|
|
42
|
+
if (features.includes(PacketTrailerFeature.PTF_USER_TIMESTAMP)) {
|
|
43
|
+
options.timestamp = true;
|
|
44
|
+
}
|
|
45
|
+
if (features.includes(PacketTrailerFeature.PTF_FRAME_ID)) {
|
|
46
|
+
options.frameId = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return options.timestamp || options.frameId ? options : undefined;
|
|
50
|
+
}
|