livekit-client 2.19.2 → 2.20.1
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 +31 -27
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +334 -126
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +2 -0
- package/dist/{livekit-client.pt.worker.js.map → livekit-client.fm.worker.js.map} +1 -1
- package/dist/{livekit-client.pt.worker.mjs → livekit-client.fm.worker.mjs} +12 -4
- package/dist/livekit-client.fm.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.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +2 -1
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +6 -5
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +1 -0
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/DataCryptor.d.ts +1 -0
- package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts +9 -8
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +1 -0
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/sifPayload.d.ts +1 -0
- package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
- package/dist/src/{packetTrailer/PacketTrailerManager.d.ts → frameMetadata/FrameMetadataManager.d.ts} +8 -4
- package/dist/src/{packetTrailer/PacketTrailerManager.d.ts.map → frameMetadata/FrameMetadataManager.d.ts.map} +1 -1
- package/dist/src/{packetTrailer/packetTrailer.d.ts → frameMetadata/frameMetadata.d.ts} +8 -7
- package/dist/src/frameMetadata/frameMetadata.d.ts.map +1 -0
- package/dist/src/{packetTrailer → frameMetadata}/types.d.ts +11 -6
- package/dist/src/frameMetadata/types.d.ts.map +1 -0
- package/dist/src/frameMetadata/utils.d.ts +9 -0
- package/dist/src/{packetTrailer → frameMetadata}/utils.d.ts.map +1 -1
- package/dist/src/frameMetadata/worker/frameMetadata.worker.d.ts +2 -0
- package/dist/src/frameMetadata/worker/frameMetadata.worker.d.ts.map +1 -0
- package/dist/src/index.d.ts +10 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +8 -4
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +41 -1
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +1 -0
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +4 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +3 -2
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/depacketizer.d.ts.map +1 -1
- package/dist/src/room/data-track/frame.d.ts +1 -0
- package/dist/src/room/data-track/frame.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/types.d.ts +1 -0
- package/dist/src/room/data-track/outgoing/types.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/extensions.d.ts +1 -0
- package/dist/src/room/data-track/packet/extensions.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/index.d.ts +1 -0
- package/dist/src/room/data-track/packet/index.d.ts.map +1 -1
- package/dist/src/room/data-track/packet/serializable.d.ts +1 -0
- package/dist/src/room/data-track/packet/serializable.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +3 -2
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +1 -0
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/participant/publishUtils.d.ts.map +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -1
- package/dist/src/room/token-source/types.d.ts +1 -0
- package/dist/src/room/token-source/types.d.ts.map +1 -1
- package/dist/src/room/token-source/utils.d.ts.map +1 -1
- package/dist/src/room/track/FrameMetadataExtractor.d.ts +19 -0
- package/dist/src/room/track/FrameMetadataExtractor.d.ts.map +1 -0
- package/dist/src/room/track/RemoteVideoTrack.d.ts +7 -7
- package/dist/src/room/track/RemoteVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/Track.d.ts +1 -0
- package/dist/src/room/track/Track.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +17 -5
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/record.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +1 -0
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
- package/dist/src/type-polyfills/non-shared-typed-arrays.d.ts.map +1 -0
- package/dist/src/utils/dataPacketBuffer.d.ts +1 -0
- package/dist/src/utils/dataPacketBuffer.d.ts.map +1 -1
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +2 -1
- package/dist/ts4.2/e2ee/types.d.ts +6 -5
- package/dist/ts4.2/e2ee/utils.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +9 -8
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +1 -0
- package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +1 -0
- package/dist/ts4.2/{packetTrailer/PacketTrailerManager.d.ts → frameMetadata/FrameMetadataManager.d.ts} +8 -4
- package/dist/ts4.2/{packetTrailer/packetTrailer.d.ts → frameMetadata/frameMetadata.d.ts} +8 -7
- package/dist/ts4.2/{packetTrailer → frameMetadata}/types.d.ts +11 -6
- package/dist/ts4.2/frameMetadata/utils.d.ts +9 -0
- package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -0
- package/dist/ts4.2/index.d.ts +9 -3
- package/dist/ts4.2/options.d.ts +8 -4
- package/dist/ts4.2/room/PCTransport.d.ts +41 -1
- package/dist/ts4.2/room/PCTransportManager.d.ts +1 -0
- package/dist/ts4.2/room/RTCEngine.d.ts +4 -1
- package/dist/ts4.2/room/Room.d.ts +3 -2
- package/dist/ts4.2/room/data-track/frame.d.ts +1 -0
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/extensions.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/index.d.ts +1 -0
- package/dist/ts4.2/room/data-track/packet/serializable.d.ts +1 -0
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +3 -2
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -0
- package/dist/ts4.2/room/token-source/types.d.ts +1 -0
- package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -0
- package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +7 -7
- package/dist/ts4.2/room/track/Track.d.ts +1 -0
- package/dist/ts4.2/room/track/options.d.ts +17 -5
- package/dist/ts4.2/room/utils.d.ts +1 -0
- package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -0
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +1 -0
- package/package.json +24 -19
- package/src/api/SignalClient.test.ts +4 -4
- package/src/api/SignalClient.ts +8 -3
- package/src/api/WebSocketStream.test.ts +7 -7
- package/src/e2ee/E2eeManager.ts +16 -12
- package/src/e2ee/types.ts +6 -5
- package/src/e2ee/utils.ts +1 -0
- package/src/e2ee/worker/DataCryptor.ts +1 -0
- package/src/e2ee/worker/FrameCryptor.test.ts +6 -5
- package/src/e2ee/worker/FrameCryptor.ts +28 -23
- package/src/e2ee/worker/ParticipantKeyHandler.test.ts +1 -0
- package/src/e2ee/worker/e2ee.worker.ts +6 -5
- package/src/e2ee/worker/naluUtils.ts +1 -0
- package/src/e2ee/worker/sifPayload.ts +1 -0
- package/src/e2ee/worker/tsconfig.json +4 -1
- package/src/{packetTrailer/PacketTrailerManager.test.ts → frameMetadata/FrameMetadataManager.test.ts} +10 -8
- package/src/{packetTrailer/PacketTrailerManager.ts → frameMetadata/FrameMetadataManager.ts} +21 -15
- package/src/{packetTrailer/packetTrailer.test.ts → frameMetadata/frameMetadata.test.ts} +84 -1
- package/src/{packetTrailer/packetTrailer.ts → frameMetadata/frameMetadata.ts} +17 -9
- package/src/{packetTrailer → frameMetadata}/types.ts +13 -6
- package/src/{packetTrailer → frameMetadata}/utils.test.ts +19 -19
- package/src/{packetTrailer → frameMetadata}/utils.ts +11 -11
- package/src/{packetTrailer/worker/packetTrailer.worker.ts → frameMetadata/worker/frameMetadata.worker.ts} +5 -5
- package/src/{packetTrailer → frameMetadata}/worker/tsconfig.json +4 -1
- package/src/index.ts +13 -2
- package/src/options.ts +9 -4
- package/src/room/PCTransport.test.ts +150 -0
- package/src/room/PCTransport.ts +117 -0
- package/src/room/PCTransportManager.ts +4 -0
- package/src/room/RTCEngine.test.ts +143 -9
- package/src/room/RTCEngine.ts +112 -31
- package/src/room/Room.ts +11 -10
- package/src/room/data-track/depacketizer.ts +1 -0
- package/src/room/data-track/frame.ts +1 -0
- package/src/room/data-track/outgoing/types.ts +1 -0
- package/src/room/data-track/packet/extensions.ts +1 -0
- package/src/room/data-track/packet/index.ts +1 -0
- package/src/room/data-track/packet/serializable.ts +1 -0
- package/src/room/participant/LocalParticipant.test.ts +19 -19
- package/src/room/participant/LocalParticipant.ts +22 -16
- package/src/room/participant/Participant.ts +1 -0
- package/src/room/participant/publishUtils.test.ts +6 -0
- package/src/room/participant/publishUtils.ts +4 -1
- package/src/room/rpc/client/RpcClientManager.ts +1 -1
- package/src/room/rpc/server/RpcServerManager.ts +1 -1
- package/src/room/token-source/TokenSource.ts +9 -23
- package/src/room/token-source/types.ts +1 -0
- package/src/room/token-source/utils.ts +1 -0
- package/src/room/track/{PacketTrailerExtractor.ts → FrameMetadataExtractor.ts} +8 -8
- package/src/room/track/RemoteVideoTrack.ts +7 -11
- package/src/room/track/Track.ts +1 -0
- package/src/room/track/options.ts +19 -5
- package/src/room/track/record.ts +1 -0
- package/src/room/utils.ts +1 -0
- package/src/type-polyfills/{non-shared-typed-arrays.d.ts → non-shared-typed-arrays.ts} +6 -1
- package/src/utils/dataPacketBuffer.ts +2 -0
- package/dist/livekit-client.pt.worker.js +0 -2
- package/dist/livekit-client.pt.worker.mjs.map +0 -1
- package/dist/src/packetTrailer/packetTrailer.d.ts.map +0 -1
- package/dist/src/packetTrailer/types.d.ts.map +0 -1
- package/dist/src/packetTrailer/utils.d.ts +0 -9
- package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts +0 -2
- package/dist/src/packetTrailer/worker/packetTrailer.worker.d.ts.map +0 -1
- package/dist/src/room/track/PacketTrailerExtractor.d.ts +0 -19
- package/dist/src/room/track/PacketTrailerExtractor.d.ts.map +0 -1
- package/dist/ts4.2/packetTrailer/utils.d.ts +0 -9
- package/dist/ts4.2/packetTrailer/worker/packetTrailer.worker.d.ts +0 -2
- package/dist/ts4.2/room/track/PacketTrailerExtractor.d.ts +0 -19
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { type MediaDescription, parse } from 'sdp-transform';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { conformBundledCodecFmtp, placeholderMidsFromTransceivers } from './PCTransport';
|
|
4
|
+
|
|
5
|
+
/** Parse the `key[=value]` pairs of an fmtp config into a comparable set. */
|
|
6
|
+
const paramSet = (config: string) => new Set(config.split(';').filter(Boolean));
|
|
7
|
+
|
|
8
|
+
const fmtpOf = (media: MediaDescription[], mid: string, payload: number) => {
|
|
9
|
+
const m = media.find((section) => `${section.mid}` === mid)!;
|
|
10
|
+
return m.fmtp.find((f) => f.payload === payload)?.config;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Predicate for conformBundledCodecFmtp based on an explicit set of mids. */
|
|
14
|
+
const placeholders = (mids: string[]) => {
|
|
15
|
+
const set = new Set(mids);
|
|
16
|
+
return (m: MediaDescription) => set.has(`${m.mid}`);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// One publisher bundle: a published mic + camera (real sends, with msid/ssrc)
|
|
20
|
+
// and their pre-populated recvonly placeholders.
|
|
21
|
+
const PUBLISHER_SDP = `v=0
|
|
22
|
+
o=- 0 0 IN IP4 127.0.0.1
|
|
23
|
+
s=-
|
|
24
|
+
t=0 0
|
|
25
|
+
a=group:BUNDLE 0 1 2 3
|
|
26
|
+
m=audio 9 UDP/TLS/RTP/SAVPF 111
|
|
27
|
+
c=IN IP4 0.0.0.0
|
|
28
|
+
a=mid:0
|
|
29
|
+
a=sendonly
|
|
30
|
+
a=msid:s mic
|
|
31
|
+
a=ssrc:1111 cname:a
|
|
32
|
+
a=rtpmap:111 opus/48000/2
|
|
33
|
+
a=fmtp:111 minptime=10;useinbandfec=1;usedtx=1
|
|
34
|
+
m=audio 9 UDP/TLS/RTP/SAVPF 111
|
|
35
|
+
c=IN IP4 0.0.0.0
|
|
36
|
+
a=mid:1
|
|
37
|
+
a=recvonly
|
|
38
|
+
a=rtpmap:111 opus/48000/2
|
|
39
|
+
a=fmtp:111 minptime=10;useinbandfec=1
|
|
40
|
+
m=video 9 UDP/TLS/RTP/SAVPF 49
|
|
41
|
+
c=IN IP4 0.0.0.0
|
|
42
|
+
a=mid:2
|
|
43
|
+
a=sendonly
|
|
44
|
+
a=msid:s cam
|
|
45
|
+
a=ssrc:2222 cname:b
|
|
46
|
+
a=rtpmap:49 H265/90000
|
|
47
|
+
a=fmtp:49 level-id=186;profile-id=1;tier-flag=0;tx-mode=SRST
|
|
48
|
+
m=video 9 UDP/TLS/RTP/SAVPF 49
|
|
49
|
+
c=IN IP4 0.0.0.0
|
|
50
|
+
a=mid:3
|
|
51
|
+
a=recvonly
|
|
52
|
+
a=rtpmap:49 H265/90000
|
|
53
|
+
a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST`;
|
|
54
|
+
|
|
55
|
+
describe('placeholderMidsFromTransceivers', () => {
|
|
56
|
+
const tr = (mid: string | null, track: unknown) =>
|
|
57
|
+
({ mid, sender: { track } }) as unknown as RTCRtpTransceiver;
|
|
58
|
+
|
|
59
|
+
it('includes transceivers with a mid and no outgoing track', () => {
|
|
60
|
+
const mids = placeholderMidsFromTransceivers([
|
|
61
|
+
tr('0', {}), // real send
|
|
62
|
+
tr('1', null), // pre-populated placeholder
|
|
63
|
+
tr('2', null), // reverted after unpublish (msid may linger in SDP, but no track)
|
|
64
|
+
tr(null, null), // not yet negotiated — no mid, excluded
|
|
65
|
+
]);
|
|
66
|
+
expect(mids).toEqual(new Set(['1', '2']));
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('conformBundledCodecFmtp', () => {
|
|
71
|
+
it('copies opus and H.265 fmtp from real sections onto placeholders', () => {
|
|
72
|
+
const { media } = parse(PUBLISHER_SDP);
|
|
73
|
+
|
|
74
|
+
conformBundledCodecFmtp(media, placeholders(['1', '3']));
|
|
75
|
+
|
|
76
|
+
expect(fmtpOf(media, '1', 111)).toBe(fmtpOf(media, '0', 111));
|
|
77
|
+
expect(paramSet(fmtpOf(media, '1', 111)!)).toContain('usedtx=1');
|
|
78
|
+
expect(fmtpOf(media, '3', 49)).toBe(fmtpOf(media, '2', 49));
|
|
79
|
+
expect(paramSet(fmtpOf(media, '3', 49)!)).toContain('level-id=186');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('never rewrites the real (non-placeholder) sections', () => {
|
|
83
|
+
const { media } = parse(PUBLISHER_SDP);
|
|
84
|
+
|
|
85
|
+
conformBundledCodecFmtp(media, placeholders(['1', '3']));
|
|
86
|
+
|
|
87
|
+
expect(fmtpOf(media, '0', 111)).toBe('minptime=10;useinbandfec=1;usedtx=1');
|
|
88
|
+
expect(paramSet(fmtpOf(media, '2', 49)!)).toContain('level-id=186');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('converges divergent placeholders even when no real section declares the payload', () => {
|
|
92
|
+
// Two placeholders that disagree (a reverted one kept level 186, a fresh one
|
|
93
|
+
// has 180) with no active send present.
|
|
94
|
+
const { media } = parse(`v=0
|
|
95
|
+
o=- 0 0 IN IP4 127.0.0.1
|
|
96
|
+
s=-
|
|
97
|
+
t=0 0
|
|
98
|
+
a=group:BUNDLE 10 11
|
|
99
|
+
m=video 9 UDP/TLS/RTP/SAVPF 49
|
|
100
|
+
a=mid:10
|
|
101
|
+
a=rtpmap:49 H265/90000
|
|
102
|
+
a=fmtp:49 level-id=186;profile-id=1;tier-flag=0;tx-mode=SRST
|
|
103
|
+
m=video 9 UDP/TLS/RTP/SAVPF 49
|
|
104
|
+
a=mid:11
|
|
105
|
+
a=rtpmap:49 H265/90000
|
|
106
|
+
a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST`);
|
|
107
|
+
|
|
108
|
+
conformBundledCodecFmtp(media, placeholders(['10', '11']));
|
|
109
|
+
|
|
110
|
+
expect(fmtpOf(media, '10', 49)).toBe(fmtpOf(media, '11', 49));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('conforms a reverted section that kept its msid (second-publish case)', () => {
|
|
114
|
+
// The real second publish: a live camera send (level 186), plus the section
|
|
115
|
+
// left over from the first (unpublished) track. Chrome keeps that section's
|
|
116
|
+
// `a=msid`/`a=ssrc` even though it no longer sends, so it must be identified
|
|
117
|
+
// as a placeholder by its transceiver (no track), not by SDP.
|
|
118
|
+
const { media } = parse(`v=0
|
|
119
|
+
o=- 0 0 IN IP4 127.0.0.1
|
|
120
|
+
s=-
|
|
121
|
+
t=0 0
|
|
122
|
+
a=group:BUNDLE 3 4 5
|
|
123
|
+
m=video 9 UDP/TLS/RTP/SAVPF 49
|
|
124
|
+
a=mid:3
|
|
125
|
+
a=sendonly
|
|
126
|
+
a=msid:s cam
|
|
127
|
+
a=ssrc:9 cname:c
|
|
128
|
+
a=rtpmap:49 H265/90000
|
|
129
|
+
a=fmtp:49 level-id=186;profile-id=1;tier-flag=0;tx-mode=SRST
|
|
130
|
+
m=video 9 UDP/TLS/RTP/SAVPF 49
|
|
131
|
+
a=mid:4
|
|
132
|
+
a=sendonly
|
|
133
|
+
a=msid:s stale
|
|
134
|
+
a=ssrc:8 cname:d
|
|
135
|
+
a=rtpmap:49 H265/90000
|
|
136
|
+
a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST
|
|
137
|
+
m=video 9 UDP/TLS/RTP/SAVPF 49
|
|
138
|
+
a=mid:5
|
|
139
|
+
a=recvonly
|
|
140
|
+
a=rtpmap:49 H265/90000
|
|
141
|
+
a=fmtp:49 level-id=180;profile-id=1;tier-flag=0;tx-mode=SRST`);
|
|
142
|
+
|
|
143
|
+
// mid 4 still has msid/ssrc in the SDP, but its transceiver has no track.
|
|
144
|
+
conformBundledCodecFmtp(media, placeholders(['4', '5']));
|
|
145
|
+
|
|
146
|
+
expect(paramSet(fmtpOf(media, '3', 49)!)).toContain('level-id=186'); // real send untouched
|
|
147
|
+
expect(fmtpOf(media, '4', 49)).toBe(fmtpOf(media, '3', 49)); // stale conformed
|
|
148
|
+
expect(fmtpOf(media, '5', 49)).toBe(fmtpOf(media, '3', 49)); // placeholder conformed
|
|
149
|
+
});
|
|
150
|
+
});
|
package/src/room/PCTransport.ts
CHANGED
|
@@ -234,6 +234,17 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
234
234
|
});
|
|
235
235
|
}
|
|
236
236
|
});
|
|
237
|
+
// The server's answer sets per-track codec params (e.g. opus `usedtx`) on
|
|
238
|
+
// the sections mapped to a published track, but leaves the pre-populated
|
|
239
|
+
// recvonly placeholder sections with defaults. Conform the placeholders so
|
|
240
|
+
// each shared payload type is consistent across the bundle, otherwise
|
|
241
|
+
// libwebrtc flags a "bundled payload type collision".
|
|
242
|
+
const placeholderMids = this.getPlaceholderMids();
|
|
243
|
+
if (placeholderMids.size > 0) {
|
|
244
|
+
conformBundledCodecFmtp(sdpParsed.media, (media) =>
|
|
245
|
+
placeholderMids.has(getMidString(media.mid!)),
|
|
246
|
+
);
|
|
247
|
+
}
|
|
237
248
|
mungedSDP = write(sdpParsed);
|
|
238
249
|
}
|
|
239
250
|
await this.setMungedSDP(sd, mungedSDP, true);
|
|
@@ -400,6 +411,17 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
400
411
|
});
|
|
401
412
|
}
|
|
402
413
|
});
|
|
414
|
+
// Conform the placeholder sections (pre-populated, or reverted after an
|
|
415
|
+
// unpublish) so every shared payload type carries identical fmtp across the
|
|
416
|
+
// bundle, otherwise libwebrtc flags a "bundled payload type collision".
|
|
417
|
+
// Detection is by transceiver (mids are stable across renegotiations) since
|
|
418
|
+
// an unpublished section keeps its `a=msid`.
|
|
419
|
+
const placeholderMids = this.getPlaceholderMids();
|
|
420
|
+
if (placeholderMids.size > 0) {
|
|
421
|
+
conformBundledCodecFmtp(sdpParsed.media, (media) =>
|
|
422
|
+
placeholderMids.has(getMidString(media.mid!)),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
403
425
|
if (this.latestOfferId > offerId) {
|
|
404
426
|
this.log.warn('latestOfferId mismatch', {
|
|
405
427
|
...this.logContext,
|
|
@@ -428,6 +450,17 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
428
450
|
return answer;
|
|
429
451
|
}
|
|
430
452
|
|
|
453
|
+
/**
|
|
454
|
+
* Returns the mids of transceivers that carry no outgoing track on this
|
|
455
|
+
* (publisher) connection: the pre-populated placeholders added by
|
|
456
|
+
* `RTCEngine.applyInitialPublisherLayout`, plus any transceiver that was used
|
|
457
|
+
* for a track and reverted on unpublish. Their codec fmtp is conformed to the
|
|
458
|
+
* published tracks so a shared payload type stays consistent across the bundle.
|
|
459
|
+
*/
|
|
460
|
+
private getPlaceholderMids(): Set<string> {
|
|
461
|
+
return placeholderMidsFromTransceivers(this._pc?.getTransceivers() ?? []);
|
|
462
|
+
}
|
|
463
|
+
|
|
431
464
|
createDataChannel(label: string, dataChannelDict: RTCDataChannelInit) {
|
|
432
465
|
return this.pc.createDataChannel(label, dataChannelDict);
|
|
433
466
|
}
|
|
@@ -498,6 +531,10 @@ export default class PCTransport extends (EventEmitter as new () => TypedEmitter
|
|
|
498
531
|
return this.pc.getStats();
|
|
499
532
|
}
|
|
500
533
|
|
|
534
|
+
getMaxMessageSize() {
|
|
535
|
+
return this._pc?.sctp?.maxMessageSize;
|
|
536
|
+
}
|
|
537
|
+
|
|
501
538
|
async getConnectedAddress(): Promise<string | undefined> {
|
|
502
539
|
if (!this._pc) {
|
|
503
540
|
return;
|
|
@@ -699,6 +736,86 @@ function ensureAudioNackAndStereo(
|
|
|
699
736
|
}
|
|
700
737
|
}
|
|
701
738
|
|
|
739
|
+
/**
|
|
740
|
+
* Returns the mids of transceivers that carry no outgoing track: the
|
|
741
|
+
* pre-populated placeholders added by `RTCEngine.applyInitialPublisherLayout`,
|
|
742
|
+
* plus any transceiver that was used for a track and reverted on unpublish. The
|
|
743
|
+
* `sender.track` check is the reliable signal — an unpublished section keeps its
|
|
744
|
+
* `a=msid` (and its stale send-derived fmtp), so it can't be told apart from a
|
|
745
|
+
* real send by SDP alone. Transceiver mids are stable across renegotiations, so
|
|
746
|
+
* this works for every offer/answer after the first.
|
|
747
|
+
* @internal
|
|
748
|
+
*/
|
|
749
|
+
export function placeholderMidsFromTransceivers(
|
|
750
|
+
transceivers: readonly RTCRtpTransceiver[],
|
|
751
|
+
): Set<string> {
|
|
752
|
+
const mids = new Set<string>();
|
|
753
|
+
for (const transceiver of transceivers) {
|
|
754
|
+
if (transceiver.mid && !transceiver.sender.track) {
|
|
755
|
+
mids.add(transceiver.mid);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return mids;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Within a BUNDLE group a payload type must map to identical codec parameters
|
|
763
|
+
* across every m-line. When the same payload type carries different fmtp between
|
|
764
|
+
* sections — e.g. opus `usedtx=1` on the published microphone but not on the
|
|
765
|
+
* pre-populated recvonly placeholders, or H.265 with different `level-id` between
|
|
766
|
+
* a published video track and a placeholder — libwebrtc flags a "bundled payload
|
|
767
|
+
* type collision".
|
|
768
|
+
*
|
|
769
|
+
* Rewrite the placeholder sections so every shared payload type carries the
|
|
770
|
+
* same fmtp. Real (non-placeholder) sections always win the canonical value, so
|
|
771
|
+
* a published track's encoder parameters are never altered. When no real
|
|
772
|
+
* section declares a payload type — e.g. a placeholder that was reused for a
|
|
773
|
+
* track and then reverted to recvonly keeps its send-derived `level-id` while
|
|
774
|
+
* fresh placeholders use the default — the placeholders still converge on the
|
|
775
|
+
* first value seen, so two placeholders can't disagree either. Only placeholder
|
|
776
|
+
* sections are ever rewritten, and it is codec-agnostic (opus, H.265, ...).
|
|
777
|
+
* `isPlaceholder` identifies the sections to conform.
|
|
778
|
+
* @internal
|
|
779
|
+
*/
|
|
780
|
+
export function conformBundledCodecFmtp(
|
|
781
|
+
media: MediaDescription[],
|
|
782
|
+
isPlaceholder: (media: MediaDescription) => boolean,
|
|
783
|
+
) {
|
|
784
|
+
// Canonical fmtp per payload type. Payload types are unique within a BUNDLE
|
|
785
|
+
// group, so keying by payload alone (across audio and video) is safe. A real
|
|
786
|
+
// section's value always takes precedence; otherwise the first placeholder
|
|
787
|
+
// value seen is used so divergent placeholders still converge.
|
|
788
|
+
const canonicalByPayload = new Map<number, string>();
|
|
789
|
+
const fromRealSection = new Set<number>();
|
|
790
|
+
for (const m of media) {
|
|
791
|
+
const placeholder = isPlaceholder(m);
|
|
792
|
+
for (const fmtp of m.fmtp ?? []) {
|
|
793
|
+
if (!placeholder) {
|
|
794
|
+
canonicalByPayload.set(fmtp.payload, fmtp.config);
|
|
795
|
+
fromRealSection.add(fmtp.payload);
|
|
796
|
+
} else if (!canonicalByPayload.has(fmtp.payload)) {
|
|
797
|
+
canonicalByPayload.set(fmtp.payload, fmtp.config);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (canonicalByPayload.size === 0) {
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// Conform placeholder sections to the canonical fmtp for each shared payload.
|
|
806
|
+
for (const m of media) {
|
|
807
|
+
if (!isPlaceholder(m)) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
for (const fmtp of m.fmtp ?? []) {
|
|
811
|
+
const config = canonicalByPayload.get(fmtp.payload);
|
|
812
|
+
if (config !== undefined && fmtp.config !== config) {
|
|
813
|
+
fmtp.config = config;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
702
819
|
function extractStereoAndNackAudioFromOffer(offer: RTCSessionDescriptionInit): {
|
|
703
820
|
stereoMids: string[];
|
|
704
821
|
nackMids: string[];
|
|
@@ -310,6 +310,10 @@ export class PCTransportManager {
|
|
|
310
310
|
return matchingTransceiver?.mid;
|
|
311
311
|
}
|
|
312
312
|
|
|
313
|
+
getMaxPublisherMessageSize() {
|
|
314
|
+
return this.publisher.getMaxMessageSize();
|
|
315
|
+
}
|
|
316
|
+
|
|
313
317
|
addPublisherTrack(track: MediaStreamTrack) {
|
|
314
318
|
return this.publisher.addTrack(track);
|
|
315
319
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { DataPacket, DataPacket_Kind, UserPacket } from '@livekit/protocol';
|
|
1
2
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import RTCEngine from './RTCEngine';
|
|
3
|
+
import RTCEngine, { DataChannelKind } from './RTCEngine';
|
|
3
4
|
import { roomOptionDefaults } from './defaults';
|
|
5
|
+
import { PublishDataError } from './errors';
|
|
4
6
|
|
|
5
7
|
describe('RTCEngine', () => {
|
|
6
8
|
const originalRTCRtpSender = window.RTCRtpSender;
|
|
@@ -56,12 +58,12 @@ describe('RTCEngine', () => {
|
|
|
56
58
|
).makeRTCConfiguration();
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
function
|
|
61
|
+
function setupFrameMetadataSender(engine: RTCEngine, sender: RTCRtpSender, opts = {}) {
|
|
60
62
|
(
|
|
61
63
|
engine as unknown as {
|
|
62
|
-
|
|
64
|
+
setupFrameMetadataSender: (sender: RTCRtpSender, opts?: unknown) => void;
|
|
63
65
|
}
|
|
64
|
-
).
|
|
66
|
+
).setupFrameMetadataSender(sender, opts);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
it('does not enable encoded insertable streams without E2EE or a packet trailer worker', () => {
|
|
@@ -128,7 +130,7 @@ describe('RTCEngine', () => {
|
|
|
128
130
|
createEncodedStreams,
|
|
129
131
|
} as unknown as RTCRtpSender;
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
setupFrameMetadataSender(engine, sender);
|
|
132
134
|
|
|
133
135
|
expect(createEncodedStreams).not.toHaveBeenCalled();
|
|
134
136
|
});
|
|
@@ -145,7 +147,7 @@ describe('RTCEngine', () => {
|
|
|
145
147
|
createEncodedStreams,
|
|
146
148
|
} as unknown as RTCRtpSender;
|
|
147
149
|
|
|
148
|
-
|
|
150
|
+
setupFrameMetadataSender(engine, sender);
|
|
149
151
|
|
|
150
152
|
expect(createEncodedStreams).not.toHaveBeenCalled();
|
|
151
153
|
});
|
|
@@ -165,7 +167,7 @@ describe('RTCEngine', () => {
|
|
|
165
167
|
createEncodedStreams,
|
|
166
168
|
} as unknown as RTCRtpSender;
|
|
167
169
|
|
|
168
|
-
|
|
170
|
+
setupFrameMetadataSender(engine, sender, { packetTrailer: { timestamp: true, frameId: true } });
|
|
169
171
|
|
|
170
172
|
expect(createEncodedStreams).toHaveBeenCalledTimes(1);
|
|
171
173
|
expect(worker.postMessage).toHaveBeenCalledWith(
|
|
@@ -185,7 +187,9 @@ describe('RTCEngine', () => {
|
|
|
185
187
|
stubScriptTransformSupport();
|
|
186
188
|
|
|
187
189
|
const transform = {};
|
|
188
|
-
const RTCRtpScriptTransform = vi.fn(()
|
|
190
|
+
const RTCRtpScriptTransform = vi.fn(function () {
|
|
191
|
+
return transform;
|
|
192
|
+
});
|
|
189
193
|
Object.defineProperty(window, 'RTCRtpScriptTransform', {
|
|
190
194
|
configurable: true,
|
|
191
195
|
value: RTCRtpScriptTransform,
|
|
@@ -207,7 +211,7 @@ describe('RTCEngine', () => {
|
|
|
207
211
|
createEncodedStreams,
|
|
208
212
|
} as unknown as RTCRtpSender;
|
|
209
213
|
|
|
210
|
-
|
|
214
|
+
setupFrameMetadataSender(engine, sender, { packetTrailer: { timestamp: true } });
|
|
211
215
|
|
|
212
216
|
expect(RTCRtpScriptTransform).toHaveBeenCalledWith(worker, {
|
|
213
217
|
kind: 'encode',
|
|
@@ -216,4 +220,134 @@ describe('RTCEngine', () => {
|
|
|
216
220
|
expect((sender as unknown as { transform: unknown }).transform).toBe(transform);
|
|
217
221
|
expect(createEncodedStreams).not.toHaveBeenCalled();
|
|
218
222
|
});
|
|
223
|
+
|
|
224
|
+
describe('sendDataPacket', () => {
|
|
225
|
+
const MAX_DATA_PACKET_SIZE = 64 * 1024 - 1; // 65535 bytes (64 KB - 1)
|
|
226
|
+
function stubConnectedEngine(
|
|
227
|
+
engine: RTCEngine,
|
|
228
|
+
maxDataPacketSize: number = MAX_DATA_PACKET_SIZE,
|
|
229
|
+
) {
|
|
230
|
+
const send = vi.fn();
|
|
231
|
+
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
232
|
+
ensurePublisherConnected: vi.fn().mockResolvedValue(undefined),
|
|
233
|
+
waitForBufferStatusLow: vi.fn().mockResolvedValue(undefined),
|
|
234
|
+
updateAndEmitDCBufferStatus: vi.fn(),
|
|
235
|
+
dataChannelForKind: vi.fn(() => ({ send })),
|
|
236
|
+
pcManager: {
|
|
237
|
+
getMaxPublisherMessageSize: vi.fn(() => maxDataPacketSize),
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
return send;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
it('rejects packets larger than the max data packet size', async () => {
|
|
244
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
245
|
+
const send = stubConnectedEngine(engine);
|
|
246
|
+
|
|
247
|
+
// The serialized packet includes protobuf framing on top of the payload, so a payload at the
|
|
248
|
+
// limit is already guaranteed to exceed it once serialized.
|
|
249
|
+
const packet = new DataPacket({
|
|
250
|
+
kind: DataPacket_Kind.RELIABLE,
|
|
251
|
+
value: {
|
|
252
|
+
case: 'user',
|
|
253
|
+
value: new UserPacket({ payload: new Uint8Array(MAX_DATA_PACKET_SIZE) }),
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
await expect(engine.sendDataPacket(packet, DataChannelKind.RELIABLE)).rejects.toBeInstanceOf(
|
|
258
|
+
PublishDataError,
|
|
259
|
+
);
|
|
260
|
+
expect(send).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('does not reject packets if the max data packet size is 0', async () => {
|
|
264
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
265
|
+
const send = stubConnectedEngine(engine, 0);
|
|
266
|
+
|
|
267
|
+
const packet = new DataPacket({
|
|
268
|
+
kind: DataPacket_Kind.RELIABLE,
|
|
269
|
+
value: {
|
|
270
|
+
case: 'user',
|
|
271
|
+
value: new UserPacket({ payload: new Uint8Array(100) }),
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
// Sending the packet should succeed, there isn't a size limit
|
|
276
|
+
await expect(
|
|
277
|
+
engine.sendDataPacket(packet, DataChannelKind.RELIABLE),
|
|
278
|
+
).resolves.toBeUndefined();
|
|
279
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('sends packets within the max data packet size', async () => {
|
|
283
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
284
|
+
const send = stubConnectedEngine(engine);
|
|
285
|
+
|
|
286
|
+
const packet = new DataPacket({
|
|
287
|
+
kind: DataPacket_Kind.RELIABLE,
|
|
288
|
+
value: {
|
|
289
|
+
case: 'user',
|
|
290
|
+
value: new UserPacket({ payload: new Uint8Array(1024) }),
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
await expect(
|
|
295
|
+
engine.sendDataPacket(packet, DataChannelKind.RELIABLE),
|
|
296
|
+
).resolves.toBeUndefined();
|
|
297
|
+
expect(send).toHaveBeenCalledTimes(1);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('handleDataChannelClose', () => {
|
|
302
|
+
function stubCloseEnv(
|
|
303
|
+
engine: RTCEngine,
|
|
304
|
+
{ closed, publisherState }: { closed: boolean; publisherState: RTCPeerConnectionState },
|
|
305
|
+
) {
|
|
306
|
+
const error = vi.fn();
|
|
307
|
+
Object.assign(engine as unknown as Record<string, unknown>, {
|
|
308
|
+
_isClosed: closed,
|
|
309
|
+
log: { error },
|
|
310
|
+
pcManager: {
|
|
311
|
+
publisher: { getConnectionState: () => publisherState },
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
return error;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function fireClose(engine: RTCEngine, kind: DataChannelKind) {
|
|
318
|
+
(
|
|
319
|
+
engine as unknown as {
|
|
320
|
+
handleDataChannelClose: (kind: DataChannelKind) => () => void;
|
|
321
|
+
}
|
|
322
|
+
).handleDataChannelClose(kind)();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
it('logs an error when a publisher channel closes while connected', () => {
|
|
326
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
327
|
+
const error = stubCloseEnv(engine, { closed: false, publisherState: 'connected' });
|
|
328
|
+
|
|
329
|
+
fireClose(engine, DataChannelKind.RELIABLE);
|
|
330
|
+
|
|
331
|
+
expect(error).toHaveBeenCalledOnce();
|
|
332
|
+
expect(error.mock.calls[0][0]).toContain('RELIABLE');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('stays quiet when the engine is already closed', () => {
|
|
336
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
337
|
+
const error = stubCloseEnv(engine, { closed: true, publisherState: 'connected' });
|
|
338
|
+
|
|
339
|
+
fireClose(engine, DataChannelKind.RELIABLE);
|
|
340
|
+
|
|
341
|
+
expect(error).not.toHaveBeenCalled();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('stays quiet when the publisher PC is no longer connected', () => {
|
|
345
|
+
const engine = new RTCEngine(roomOptionDefaults);
|
|
346
|
+
const error = stubCloseEnv(engine, { closed: false, publisherState: 'closed' });
|
|
347
|
+
|
|
348
|
+
fireClose(engine, DataChannelKind.RELIABLE);
|
|
349
|
+
|
|
350
|
+
expect(error).not.toHaveBeenCalled();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
219
353
|
});
|