livekit-client 2.22.3 → 2.23.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/README.md +35 -0
- 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 +71 -8
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +1746 -547
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.fm.worker.js +1 -1
- package/dist/livekit-client.fm.worker.js.map +1 -1
- package/dist/livekit-client.fm.worker.mjs +2 -1
- package/dist/livekit-client.fm.worker.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +4 -2
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/WebSocketStream.d.ts +1 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -1
- package/dist/src/api/utils.d.ts +1 -1
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/worker/FrameCryptor.d.ts.map +1 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts +12 -1
- package/dist/src/e2ee/worker/naluUtils.d.ts.map +1 -1
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/options.d.ts +14 -0
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/DefaultReconnectPolicy.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +100 -7
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +16 -0
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +54 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-channel/DataChannelManager.d.ts +9 -8
- package/dist/src/room/data-channel/DataChannelManager.d.ts.map +1 -1
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts +25 -12
- package/dist/src/room/data-channel/FlowControlledDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-channel/LossyDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-channel/ReliableDataChannel.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +18 -0
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/events.d.ts +3 -2
- package/dist/src/room/events.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts +4 -0
- 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/publishUtils.d.ts.map +1 -1
- package/dist/src/room/rpc/client/RpcClientManager.d.ts.map +1 -1
- package/dist/src/room/rpc/utils.d.ts.map +1 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts +24 -1
- package/dist/src/room/track/LocalAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalTrack.d.ts +18 -0
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/LocalVideoTrack.d.ts +9 -0
- package/dist/src/room/track/LocalVideoTrack.d.ts.map +1 -1
- package/dist/src/room/track/RemoteAudioTrack.d.ts +6 -0
- package/dist/src/room/track/RemoteAudioTrack.d.ts.map +1 -1
- package/dist/src/room/track/processor/types.d.ts +11 -0
- package/dist/src/room/track/processor/types.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts +13 -0
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +60 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/test/compressionStreamStubs.d.ts +12 -0
- package/dist/src/test/compressionStreamStubs.d.ts.map +1 -0
- package/dist/ts4.2/api/SignalClient.d.ts +187 -185
- package/dist/ts4.2/api/SignalClientStateMachine.d.ts +85 -85
- package/dist/ts4.2/api/WebSocketStream.d.ts +34 -33
- package/dist/ts4.2/api/utils.d.ts +9 -9
- package/dist/ts4.2/connectionHelper/ConnectionCheck.d.ts +30 -30
- package/dist/ts4.2/connectionHelper/checks/Checker.d.ts +62 -62
- package/dist/ts4.2/connectionHelper/checks/cloudRegion.d.ts +18 -18
- package/dist/ts4.2/connectionHelper/checks/connectionProtocol.d.ts +20 -20
- package/dist/ts4.2/connectionHelper/checks/publishAudio.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/publishVideo.d.ts +7 -7
- package/dist/ts4.2/connectionHelper/checks/reconnect.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/turn.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/webrtc.d.ts +6 -6
- package/dist/ts4.2/connectionHelper/checks/websocket.d.ts +6 -6
- package/dist/ts4.2/e2ee/E2eeManager.d.ts +111 -111
- package/dist/ts4.2/e2ee/KeyProvider.d.ts +54 -54
- package/dist/ts4.2/e2ee/constants.d.ts +21 -21
- package/dist/ts4.2/e2ee/errors.d.ts +12 -12
- package/dist/ts4.2/e2ee/events.d.ts +39 -39
- package/dist/ts4.2/e2ee/index.d.ts +6 -6
- package/dist/ts4.2/e2ee/types.d.ts +231 -231
- package/dist/ts4.2/e2ee/utils.d.ts +32 -32
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +16 -16
- package/dist/ts4.2/e2ee/worker/ErrorRateLimiter.d.ts +21 -21
- package/dist/ts4.2/e2ee/worker/FrameCryptor.d.ts +209 -209
- package/dist/ts4.2/e2ee/worker/ParticipantKeyHandler.d.ts +81 -84
- package/dist/ts4.2/e2ee/worker/e2ee.worker.d.ts +2 -2
- package/dist/ts4.2/e2ee/worker/naluUtils.d.ts +39 -28
- package/dist/ts4.2/e2ee/worker/sifPayload.d.ts +23 -23
- package/dist/ts4.2/frameMetadata/FrameMetadataManager.d.ts +53 -53
- package/dist/ts4.2/frameMetadata/frameMetadata.d.ts +33 -33
- package/dist/ts4.2/frameMetadata/types.d.ts +62 -62
- package/dist/ts4.2/frameMetadata/utils.d.ts +9 -9
- package/dist/ts4.2/frameMetadata/worker/frameMetadata.worker.d.ts +2 -2
- package/dist/ts4.2/index.d.ts +76 -76
- package/dist/ts4.2/logger.d.ts +66 -66
- package/dist/ts4.2/options.d.ts +163 -149
- package/dist/ts4.2/room/BackOffStrategy.d.ts +27 -27
- package/dist/ts4.2/room/DefaultReconnectPolicy.d.ts +8 -8
- package/dist/ts4.2/room/DeviceManager.d.ts +12 -12
- package/dist/ts4.2/room/PCTransport.d.ts +268 -175
- package/dist/ts4.2/room/PCTransportManager.d.ts +77 -77
- package/dist/ts4.2/room/RTCEngine.d.ts +277 -262
- package/dist/ts4.2/room/ReconnectPolicy.d.ts +24 -24
- package/dist/ts4.2/room/RegionUrlProvider.d.ts +36 -36
- package/dist/ts4.2/room/Room.d.ts +407 -365
- package/dist/ts4.2/room/attribute-typings.d.ts +35 -35
- package/dist/ts4.2/room/data-channel/DataChannelManager.d.ts +63 -62
- package/dist/ts4.2/room/data-channel/FlowControlledDataChannel.d.ts +107 -95
- package/dist/ts4.2/room/data-channel/LossyDataChannel.d.ts +41 -41
- package/dist/ts4.2/room/data-channel/ReliableDataChannel.d.ts +57 -57
- package/dist/ts4.2/room/data-channel/types.d.ts +12 -12
- package/dist/ts4.2/room/data-stream/compression.d.ts +49 -49
- package/dist/ts4.2/room/data-stream/constants.d.ts +18 -18
- package/dist/ts4.2/room/data-stream/incoming/IncomingDataStreamManager.d.ts +29 -29
- package/dist/ts4.2/room/data-stream/incoming/StreamReader.d.ts +80 -80
- package/dist/ts4.2/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts +73 -73
- package/dist/ts4.2/room/data-stream/outgoing/StreamWriter.d.ts +16 -16
- package/dist/ts4.2/room/data-stream/outgoing/header-utils.d.ts +18 -18
- package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +79 -80
- package/dist/ts4.2/room/data-track/RemoteDataTrack.d.ts +50 -50
- package/dist/ts4.2/room/data-track/depacketizer.d.ts +59 -59
- package/dist/ts4.2/room/data-track/frame.d.ts +22 -22
- package/dist/ts4.2/room/data-track/handle.d.ts +27 -27
- package/dist/ts4.2/room/data-track/incoming/IncomingDataTrackManager.d.ts +124 -124
- package/dist/ts4.2/room/data-track/incoming/errors.d.ts +24 -24
- package/dist/ts4.2/room/data-track/incoming/pipeline.d.ts +41 -41
- package/dist/ts4.2/room/data-track/incoming/types.d.ts +20 -20
- package/dist/ts4.2/room/data-track/outgoing/OutgoingDataTrackManager.d.ts +126 -126
- package/dist/ts4.2/room/data-track/outgoing/errors.d.ts +74 -74
- package/dist/ts4.2/room/data-track/outgoing/pipeline.d.ts +23 -23
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +48 -48
- package/dist/ts4.2/room/data-track/packet/constants.d.ts +20 -20
- package/dist/ts4.2/room/data-track/packet/errors.d.ts +40 -40
- package/dist/ts4.2/room/data-track/packet/extensions.d.ts +69 -69
- package/dist/ts4.2/room/data-track/packet/index.d.ts +98 -98
- package/dist/ts4.2/room/data-track/packet/serializable.d.ts +13 -13
- package/dist/ts4.2/room/data-track/packetizer.d.ts +43 -43
- package/dist/ts4.2/room/data-track/track-interfaces.d.ts +23 -23
- package/dist/ts4.2/room/data-track/types.d.ts +22 -22
- package/dist/ts4.2/room/data-track/utils.d.ts +60 -60
- package/dist/ts4.2/room/debounce.d.ts +11 -11
- package/dist/ts4.2/room/defaults.d.ts +9 -9
- package/dist/ts4.2/room/errors.d.ts +180 -162
- package/dist/ts4.2/room/events.d.ts +638 -637
- package/dist/ts4.2/room/participant/LocalParticipant.d.ts +291 -287
- package/dist/ts4.2/room/participant/Participant.d.ts +142 -144
- package/dist/ts4.2/room/participant/ParticipantTrackPermission.d.ts +26 -26
- package/dist/ts4.2/room/participant/RemoteParticipant.d.ts +85 -86
- package/dist/ts4.2/room/participant/publishUtils.d.ts +53 -53
- package/dist/ts4.2/room/rpc/client/RpcClientManager.d.ts +43 -43
- package/dist/ts4.2/room/rpc/client/events.d.ts +8 -8
- package/dist/ts4.2/room/rpc/index.d.ts +7 -7
- package/dist/ts4.2/room/rpc/server/RpcServerManager.d.ts +44 -44
- package/dist/ts4.2/room/rpc/server/events.d.ts +8 -8
- package/dist/ts4.2/room/rpc/utils.d.ts +131 -131
- package/dist/ts4.2/room/stats.d.ts +71 -71
- package/dist/ts4.2/room/statsSummary.d.ts +13 -13
- package/dist/ts4.2/room/timers.d.ts +12 -12
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +82 -82
- package/dist/ts4.2/room/token-source/types.d.ts +70 -70
- package/dist/ts4.2/room/token-source/utils.d.ts +8 -8
- package/dist/ts4.2/room/track/FrameMetadataExtractor.d.ts +19 -19
- package/dist/ts4.2/room/track/LocalAudioTrack.d.ts +62 -42
- package/dist/ts4.2/room/track/LocalTrack.d.ts +143 -125
- package/dist/ts4.2/room/track/LocalTrackPublication.d.ts +42 -42
- package/dist/ts4.2/room/track/LocalVideoTrack.d.ts +109 -100
- package/dist/ts4.2/room/track/RemoteAudioTrack.d.ts +61 -55
- package/dist/ts4.2/room/track/RemoteTrack.d.ts +38 -38
- package/dist/ts4.2/room/track/RemoteTrackPublication.d.ts +72 -75
- package/dist/ts4.2/room/track/RemoteVideoTrack.d.ts +73 -76
- package/dist/ts4.2/room/track/Track.d.ts +156 -166
- package/dist/ts4.2/room/track/TrackPublication.d.ts +75 -81
- package/dist/ts4.2/room/track/create.d.ts +25 -25
- package/dist/ts4.2/room/track/facingMode.d.ts +41 -41
- package/dist/ts4.2/room/track/options.d.ts +403 -403
- package/dist/ts4.2/room/track/processor/types.d.ts +42 -31
- package/dist/ts4.2/room/track/record.d.ts +13 -13
- package/dist/ts4.2/room/track/types.d.ts +28 -28
- package/dist/ts4.2/room/track/utils.d.ts +60 -47
- package/dist/ts4.2/room/types.d.ts +127 -127
- package/dist/ts4.2/room/utils.d.ts +273 -214
- package/dist/ts4.2/test/MockMediaStreamTrack.d.ts +26 -26
- package/dist/ts4.2/test/compressionStreamStubs.d.ts +12 -0
- package/dist/ts4.2/test/mocks.d.ts +12 -12
- package/dist/ts4.2/test/promiseState.d.ts +12 -12
- package/dist/ts4.2/test/signalToken.d.ts +26 -26
- package/dist/ts4.2/type-polyfills/non-shared-typed-arrays.d.ts +2 -2
- package/dist/ts4.2/utils/AsyncQueue.d.ts +23 -23
- package/dist/ts4.2/utils/TypedPromise.d.ts +17 -17
- package/dist/ts4.2/utils/abort-signal-polyfill.d.ts +13 -13
- package/dist/ts4.2/utils/browserParser.d.ts +13 -13
- package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -8
- package/dist/ts4.2/utils/cloneDeep.d.ts +2 -2
- package/dist/ts4.2/utils/dataPacketBuffer.d.ts +34 -34
- package/dist/ts4.2/utils/deferrable-map.d.ts +32 -32
- package/dist/ts4.2/utils/machineInspector.d.ts +54 -54
- package/dist/ts4.2/utils/serializer.d.ts +48 -48
- package/dist/ts4.2/utils/subscribeToEvents.d.ts +15 -15
- package/dist/ts4.2/utils/ttlmap.d.ts +20 -20
- package/dist/ts4.2/version.d.ts +15 -15
- package/package.json +5 -4
- package/src/api/SignalClient.e2e.test.ts +1 -3
- package/src/api/SignalClient.test.ts +394 -10
- package/src/api/SignalClient.ts +132 -76
- package/src/api/WebSocketStream.test.ts +11 -0
- package/src/api/WebSocketStream.ts +22 -1
- package/src/api/utils.ts +1 -1
- package/src/e2ee/E2eeManager.test.ts +43 -0
- package/src/e2ee/E2eeManager.ts +6 -2
- package/src/e2ee/worker/FrameCryptor.test.ts +212 -90
- package/src/e2ee/worker/FrameCryptor.ts +21 -4
- package/src/e2ee/worker/naluUtils.test.ts +123 -0
- package/src/e2ee/worker/naluUtils.ts +57 -2
- package/src/index.ts +2 -0
- package/src/options.ts +16 -0
- package/src/room/DefaultReconnectPolicy.test.ts +58 -0
- package/src/room/DefaultReconnectPolicy.ts +14 -2
- package/src/room/PCTransport.test.ts +327 -9
- package/src/room/PCTransport.ts +339 -36
- package/src/room/PCTransportManager.ts +3 -1
- package/src/room/RTCEngine.test.ts +318 -2
- package/src/room/RTCEngine.ts +126 -12
- package/src/room/Room.test.ts +148 -6
- package/src/room/Room.ts +312 -80
- package/src/room/data-channel/DataChannelManager.test.ts +24 -7
- package/src/room/data-channel/DataChannelManager.ts +43 -38
- package/src/room/data-channel/FlowControlledDataChannel.test.ts +60 -18
- package/src/room/data-channel/FlowControlledDataChannel.ts +40 -22
- package/src/room/data-channel/LossyDataChannel.test.ts +38 -0
- package/src/room/data-channel/LossyDataChannel.ts +10 -1
- package/src/room/data-channel/ReliableDataChannel.test.ts +72 -0
- package/src/room/data-channel/ReliableDataChannel.ts +16 -5
- package/src/room/data-stream/incoming/IncomingDataStreamManager.test.ts +47 -20
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +16 -5
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.test.ts +60 -39
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +4 -4
- package/src/room/defaults.ts +1 -0
- package/src/room/errors.test.ts +36 -0
- package/src/room/errors.ts +27 -0
- package/src/room/events.ts +2 -1
- package/src/room/participant/LocalParticipant.test.ts +136 -1
- package/src/room/participant/LocalParticipant.ts +66 -17
- package/src/room/participant/Participant.ts +6 -3
- package/src/room/participant/publishUtils.ts +0 -19
- package/src/room/participant/restartOnDefaultAudioDevice.test.ts +177 -0
- package/src/room/rpc/client/RpcClientManager.test.ts +51 -0
- package/src/room/rpc/client/RpcClientManager.ts +32 -15
- package/src/room/rpc/utils.test.ts +32 -0
- package/src/room/rpc/utils.ts +7 -0
- package/src/room/track/LocalAudioTrack.ts +101 -10
- package/src/room/track/LocalTrack.ts +87 -19
- package/src/room/track/LocalVideoTrack.ts +17 -3
- package/src/room/track/RemoteAudioTrack.ts +59 -14
- package/src/room/track/RemoteVideoTrack.test.ts +210 -0
- package/src/room/track/RemoteVideoTrack.ts +29 -3
- package/src/room/track/processor/types.ts +11 -0
- package/src/room/track/utils.test.ts +206 -2
- package/src/room/track/utils.ts +117 -5
- package/src/room/utils.test.ts +250 -0
- package/src/room/utils.ts +152 -19
- package/src/test/compressionStreamStubs.ts +53 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vitest } from 'vitest';
|
|
2
2
|
import { appendPacketTrailer, extractPacketTrailer } from '../../frameMetadata/frameMetadata';
|
|
3
3
|
import type { FrameMetadataPublishOptions } from '../../frameMetadata/types';
|
|
4
|
+
import { workerLogger } from '../../logger';
|
|
5
|
+
import type { VideoCodec } from '../../room/track/options';
|
|
4
6
|
import type { NonSharedUint8Array } from '../../type-polyfills/non-shared-typed-arrays';
|
|
5
7
|
import { IV_LENGTH, KEY_PROVIDER_DEFAULTS } from '../constants';
|
|
6
8
|
import { CryptorEvent } from '../events';
|
|
@@ -71,12 +73,14 @@ function prepareParticipantTestEncoder(
|
|
|
71
73
|
participantIdentity: string,
|
|
72
74
|
partialKeyProviderOptions: Partial<KeyProviderOptions>,
|
|
73
75
|
packetTrailer?: FrameMetadataPublishOptions,
|
|
76
|
+
codec?: VideoCodec,
|
|
74
77
|
) {
|
|
75
78
|
return prepareParticipantTest(
|
|
76
79
|
'encode',
|
|
77
80
|
participantIdentity,
|
|
78
81
|
partialKeyProviderOptions,
|
|
79
82
|
packetTrailer,
|
|
83
|
+
codec,
|
|
80
84
|
);
|
|
81
85
|
}
|
|
82
86
|
|
|
@@ -85,6 +89,7 @@ function prepareParticipantTest(
|
|
|
85
89
|
participantIdentity: string,
|
|
86
90
|
partialKeyProviderOptions: Partial<KeyProviderOptions>,
|
|
87
91
|
packetTrailer?: FrameMetadataPublishOptions,
|
|
92
|
+
codec?: VideoCodec,
|
|
88
93
|
): {
|
|
89
94
|
keys: ParticipantKeyHandler;
|
|
90
95
|
cryptor: FrameCryptor;
|
|
@@ -110,7 +115,7 @@ function prepareParticipantTest(
|
|
|
110
115
|
new ReadableStream(input),
|
|
111
116
|
new WritableStream(output),
|
|
112
117
|
'testTrack',
|
|
113
|
-
|
|
118
|
+
codec,
|
|
114
119
|
packetTrailer,
|
|
115
120
|
);
|
|
116
121
|
|
|
@@ -233,81 +238,224 @@ describe('FrameCryptor', () => {
|
|
|
233
238
|
}
|
|
234
239
|
});
|
|
235
240
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
);
|
|
246
|
-
|
|
247
|
-
|
|
241
|
+
describe('H.264 start codes (#2140)', () => {
|
|
242
|
+
const sps = [0x67, 0x64, 0x00, 0x1f, 0xac, 0xd9];
|
|
243
|
+
const pps = [0x68, 0xee, 0x3c, 0xb0];
|
|
244
|
+
const idr = [0x65, 0x88, 0x84, 0x21, 0x43, 0x55, 0x66, 0x77];
|
|
245
|
+
|
|
246
|
+
async function encryptAndDecrypt(plainTextData: NonSharedUint8Array) {
|
|
247
|
+
const encoder = prepareParticipantTestEncoder(participantIdentity, {});
|
|
248
|
+
await encoder.keys.setKey(await createKeyMaterialFromString('key1'), 1);
|
|
249
|
+
const encrypted = mockRTCEncodedVideoFrame(plainTextData);
|
|
250
|
+
encoder.input.write(encrypted);
|
|
251
|
+
await vitest.waitFor(() => expect(encoder.output.chunks).toHaveLength(1));
|
|
252
|
+
const encryptedData = new Uint8Array(encrypted.data.slice(0));
|
|
253
|
+
|
|
254
|
+
const decoder = prepareParticipantTestDecoder(participantIdentity, {});
|
|
255
|
+
await decoder.keys.setKey(await createKeyMaterialFromString('key1'), 1);
|
|
256
|
+
const decrypted = mockRTCEncodedVideoFrame(new Uint8Array(encryptedData));
|
|
257
|
+
decoder.input.write(decrypted);
|
|
258
|
+
await vitest.waitFor(() => expect(decoder.output.chunks).toHaveLength(1));
|
|
259
|
+
|
|
260
|
+
return { encryptedData, decryptedData: new Uint8Array(decrypted.data) };
|
|
261
|
+
}
|
|
248
262
|
|
|
249
|
-
|
|
250
|
-
|
|
263
|
+
it('rewrites 3-byte start codes in the unencrypted header to 4-byte start codes', async () => {
|
|
264
|
+
const { encryptedData, decryptedData } = await encryptAndDecrypt(
|
|
265
|
+
new Uint8Array([0, 0, 1, ...sps, 0, 0, 1, ...pps, 0, 0, 1, ...idr]),
|
|
251
266
|
);
|
|
252
267
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
expect(extracted.metadata?.userTimestamp).toBeGreaterThanOrEqual(
|
|
258
|
-
BigInt(now.getTime()) * BigInt(1000),
|
|
268
|
+
// The unencrypted header runs through the first 2 bytes of the slice NALU.
|
|
269
|
+
const expectedHeader = [0, 0, 0, 1, ...sps, 0, 0, 0, 1, ...pps, 0, 0, 0, 1, 0x65, 0x88];
|
|
270
|
+
expect(Array.from(encryptedData.subarray(0, expectedHeader.length))).toEqual(
|
|
271
|
+
expectedHeader,
|
|
259
272
|
);
|
|
260
|
-
expect(
|
|
273
|
+
expect(Array.from(decryptedData)).toEqual([
|
|
274
|
+
0,
|
|
275
|
+
0,
|
|
276
|
+
0,
|
|
277
|
+
1,
|
|
278
|
+
...sps,
|
|
279
|
+
0,
|
|
280
|
+
0,
|
|
281
|
+
0,
|
|
282
|
+
1,
|
|
283
|
+
...pps,
|
|
284
|
+
0,
|
|
285
|
+
0,
|
|
286
|
+
0,
|
|
287
|
+
1,
|
|
288
|
+
...idr,
|
|
289
|
+
]);
|
|
290
|
+
});
|
|
261
291
|
|
|
262
|
-
|
|
263
|
-
|
|
292
|
+
it('rewrites a 3-byte start code on a delta frame', async () => {
|
|
293
|
+
const pSlice = [0x41, 0x9a, 0x21, 0x43, 0x55, 0x66];
|
|
294
|
+
const { encryptedData, decryptedData } = await encryptAndDecrypt(
|
|
295
|
+
new Uint8Array([0, 0, 1, ...pSlice]),
|
|
264
296
|
);
|
|
265
|
-
expect(frameTrailer[0]).toEqual(IV_LENGTH);
|
|
266
|
-
expect(frameTrailer[1]).toEqual(1);
|
|
267
|
-
} finally {
|
|
268
|
-
vitest.useRealTimers();
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
297
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const { keys, input, output } = prepareParticipantTestEncoder(
|
|
276
|
-
participantIdentity,
|
|
277
|
-
{},
|
|
278
|
-
{ timestamp: false, frameId: false },
|
|
279
|
-
);
|
|
298
|
+
expect(Array.from(encryptedData.subarray(0, 6))).toEqual([0, 0, 0, 1, 0x41, 0x9a]);
|
|
299
|
+
expect(Array.from(decryptedData)).toEqual([0, 0, 0, 1, ...pSlice]);
|
|
300
|
+
});
|
|
280
301
|
|
|
281
|
-
|
|
302
|
+
it('leaves 4-byte start codes in the unencrypted header unchanged', async () => {
|
|
303
|
+
const plainTextData = [0, 0, 0, 1, ...sps, 0, 0, 0, 1, ...pps, 0, 0, 0, 1, ...idr];
|
|
304
|
+
const { encryptedData, decryptedData } = await encryptAndDecrypt(
|
|
305
|
+
new Uint8Array(plainTextData),
|
|
306
|
+
);
|
|
282
307
|
|
|
283
|
-
const
|
|
284
|
-
|
|
308
|
+
const expectedHeader = [0, 0, 0, 1, ...sps, 0, 0, 0, 1, ...pps, 0, 0, 0, 1, 0x65, 0x88];
|
|
309
|
+
expect(Array.from(encryptedData.subarray(0, expectedHeader.length))).toEqual(
|
|
310
|
+
expectedHeader,
|
|
285
311
|
);
|
|
312
|
+
expect(Array.from(decryptedData)).toEqual(plainTextData);
|
|
313
|
+
});
|
|
314
|
+
it('logs the frame bytes once when NALU processing fails', async () => {
|
|
315
|
+
vitest.useFakeTimers();
|
|
316
|
+
const warn = vitest.spyOn(workerLogger, 'warn');
|
|
317
|
+
try {
|
|
318
|
+
const { keys, input, output } = prepareParticipantTestEncoder(
|
|
319
|
+
participantIdentity,
|
|
320
|
+
{},
|
|
321
|
+
undefined,
|
|
322
|
+
'h264',
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
await keys.setKey(await createKeyMaterialFromString('key1'), 1);
|
|
326
|
+
|
|
327
|
+
// No start code at offset 0, so findNALUIndices throws 'byte stream contains
|
|
328
|
+
// leading data' and the cryptor falls back to VP8 handling.
|
|
329
|
+
input.write(
|
|
330
|
+
mockRTCEncodedVideoFrame(new Uint8Array([9, 9, 9, 9, 1, 2, 3, 4, 5, 6, 7, 8])),
|
|
331
|
+
);
|
|
332
|
+
await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
|
|
333
|
+
|
|
334
|
+
expect(warn).toHaveBeenCalledWith(
|
|
335
|
+
'NALU processing failed, falling back to VP8 handling',
|
|
336
|
+
expect.objectContaining({
|
|
337
|
+
detectedCodec: 'h264',
|
|
338
|
+
frameType: 'key',
|
|
339
|
+
byteLength: 12,
|
|
340
|
+
firstBytes: expect.stringMatching(/^09 09 09 09 /),
|
|
341
|
+
}),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// same payload type, so the once-per-tuple guard must suppress the second log
|
|
345
|
+
input.write(
|
|
346
|
+
mockRTCEncodedVideoFrame(new Uint8Array([8, 8, 8, 8, 1, 2, 3, 4, 5, 6, 7, 8])),
|
|
347
|
+
);
|
|
348
|
+
await vitest.waitFor(() => expect(output.chunks).toHaveLength(2));
|
|
349
|
+
|
|
350
|
+
expect(warn).toHaveBeenCalledTimes(1);
|
|
351
|
+
} finally {
|
|
352
|
+
warn.mockRestore();
|
|
353
|
+
vitest.useRealTimers();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
286
356
|
|
|
287
|
-
|
|
288
|
-
|
|
357
|
+
it('appends packet trailer after encryption', async () => {
|
|
358
|
+
vitest.useFakeTimers();
|
|
359
|
+
const now = new Date('2025-04-10T12:00:00.123Z');
|
|
360
|
+
vitest.setSystemTime(now);
|
|
361
|
+
try {
|
|
362
|
+
const { keys, input, output } = prepareParticipantTestEncoder(
|
|
363
|
+
participantIdentity,
|
|
364
|
+
{},
|
|
365
|
+
{ timestamp: true, frameId: true },
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
await keys.setKey(await createKeyMaterialFromString('key1'), 1);
|
|
369
|
+
|
|
370
|
+
const frame = mockRTCEncodedVideoFrame(
|
|
371
|
+
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
input.write(frame);
|
|
375
|
+
await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
|
|
376
|
+
|
|
377
|
+
const extracted = extractPacketTrailer(frame.data);
|
|
378
|
+
expect(extracted.metadata?.userTimestamp).toBeGreaterThanOrEqual(
|
|
379
|
+
BigInt(now.getTime()) * BigInt(1000),
|
|
380
|
+
);
|
|
381
|
+
expect(extracted.metadata?.frameId).toBe(1);
|
|
382
|
+
|
|
383
|
+
const frameTrailer = new Uint8Array(
|
|
384
|
+
extracted.data.buffer.slice(extracted.data.byteLength - 2),
|
|
385
|
+
);
|
|
386
|
+
expect(frameTrailer[0]).toEqual(IV_LENGTH);
|
|
387
|
+
expect(frameTrailer[1]).toEqual(1);
|
|
388
|
+
} finally {
|
|
389
|
+
vitest.useRealTimers();
|
|
390
|
+
}
|
|
391
|
+
});
|
|
289
392
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
393
|
+
it('does not append packet trailer after encryption when no trailer features are enabled', async () => {
|
|
394
|
+
vitest.useFakeTimers();
|
|
395
|
+
try {
|
|
396
|
+
const { keys, input, output } = prepareParticipantTestEncoder(
|
|
397
|
+
participantIdentity,
|
|
398
|
+
{},
|
|
399
|
+
{ timestamp: false, frameId: false },
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
await keys.setKey(await createKeyMaterialFromString('key1'), 1);
|
|
403
|
+
|
|
404
|
+
const frame = mockRTCEncodedVideoFrame(
|
|
405
|
+
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
|
|
406
|
+
);
|
|
407
|
+
|
|
408
|
+
input.write(frame);
|
|
409
|
+
await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
|
|
410
|
+
|
|
411
|
+
const extracted = extractPacketTrailer(frame.data);
|
|
412
|
+
expect(extracted.metadata).toBeUndefined();
|
|
413
|
+
expect(extracted.data.byteLength).toBe(frame.data.byteLength);
|
|
414
|
+
|
|
415
|
+
const frameTrailer = new Uint8Array(frame.data.slice(frame.data.byteLength - 2));
|
|
416
|
+
expect(frameTrailer[0]).toEqual(IV_LENGTH);
|
|
417
|
+
expect(frameTrailer[1]).toEqual(1);
|
|
418
|
+
} finally {
|
|
419
|
+
vitest.useRealTimers();
|
|
420
|
+
}
|
|
421
|
+
});
|
|
293
422
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
423
|
+
it('appends only timestamp packet trailer metadata after encryption', async () => {
|
|
424
|
+
vitest.useFakeTimers();
|
|
425
|
+
const now = new Date('2025-04-10T12:00:00.123Z');
|
|
426
|
+
vitest.setSystemTime(now);
|
|
427
|
+
try {
|
|
428
|
+
const { keys, input, output } = prepareParticipantTestEncoder(
|
|
429
|
+
participantIdentity,
|
|
430
|
+
{},
|
|
431
|
+
{ timestamp: true },
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
await keys.setKey(await createKeyMaterialFromString('key1'), 1);
|
|
435
|
+
|
|
436
|
+
const frame = mockRTCEncodedVideoFrame(
|
|
437
|
+
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
input.write(frame);
|
|
441
|
+
await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
|
|
442
|
+
|
|
443
|
+
const extracted = extractPacketTrailer(frame.data);
|
|
444
|
+
expect(frame.data.byteLength - extracted.data.byteLength).toBe(15);
|
|
445
|
+
expect(extracted.metadata?.userTimestamp).toBeGreaterThanOrEqual(
|
|
446
|
+
BigInt(now.getTime()) * BigInt(1000),
|
|
447
|
+
);
|
|
448
|
+
expect(extracted.metadata?.frameId).toBe(0);
|
|
449
|
+
} finally {
|
|
450
|
+
vitest.useRealTimers();
|
|
451
|
+
}
|
|
452
|
+
});
|
|
301
453
|
|
|
302
|
-
|
|
303
|
-
vitest.useFakeTimers();
|
|
304
|
-
const now = new Date('2025-04-10T12:00:00.123Z');
|
|
305
|
-
vitest.setSystemTime(now);
|
|
306
|
-
try {
|
|
454
|
+
it('appends only frame id packet trailer metadata after encryption', async () => {
|
|
307
455
|
const { keys, input, output } = prepareParticipantTestEncoder(
|
|
308
456
|
participantIdentity,
|
|
309
457
|
{},
|
|
310
|
-
{
|
|
458
|
+
{ frameId: true },
|
|
311
459
|
);
|
|
312
460
|
|
|
313
461
|
await keys.setKey(await createKeyMaterialFromString('key1'), 1);
|
|
@@ -320,37 +468,11 @@ describe('FrameCryptor', () => {
|
|
|
320
468
|
await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
|
|
321
469
|
|
|
322
470
|
const extracted = extractPacketTrailer(frame.data);
|
|
323
|
-
expect(frame.data.byteLength - extracted.data.byteLength).toBe(
|
|
324
|
-
expect(extracted.metadata
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
} finally {
|
|
329
|
-
vitest.useRealTimers();
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it('appends only frame id packet trailer metadata after encryption', async () => {
|
|
334
|
-
const { keys, input, output } = prepareParticipantTestEncoder(
|
|
335
|
-
participantIdentity,
|
|
336
|
-
{},
|
|
337
|
-
{ frameId: true },
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
await keys.setKey(await createKeyMaterialFromString('key1'), 1);
|
|
341
|
-
|
|
342
|
-
const frame = mockRTCEncodedVideoFrame(
|
|
343
|
-
new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]),
|
|
344
|
-
);
|
|
345
|
-
|
|
346
|
-
input.write(frame);
|
|
347
|
-
await vitest.waitFor(() => expect(output.chunks).toHaveLength(1));
|
|
348
|
-
|
|
349
|
-
const extracted = extractPacketTrailer(frame.data);
|
|
350
|
-
expect(frame.data.byteLength - extracted.data.byteLength).toBe(11);
|
|
351
|
-
expect(extracted.metadata).toEqual({
|
|
352
|
-
userTimestamp: BigInt(0),
|
|
353
|
-
frameId: 1,
|
|
471
|
+
expect(frame.data.byteLength - extracted.data.byteLength).toBe(11);
|
|
472
|
+
expect(extracted.metadata).toEqual({
|
|
473
|
+
userTimestamp: BigInt(0),
|
|
474
|
+
frameId: 1,
|
|
475
|
+
});
|
|
354
476
|
});
|
|
355
477
|
});
|
|
356
478
|
});
|
|
@@ -25,7 +25,7 @@ import type {
|
|
|
25
25
|
import { deriveKeys, isVideoFrame, needsRbspUnescaping, parseRbsp, writeRbsp } from '../utils';
|
|
26
26
|
import { ErrorRateLimiter } from './ErrorRateLimiter';
|
|
27
27
|
import type { ParticipantKeyHandler } from './ParticipantKeyHandler';
|
|
28
|
-
import { processNALUsForEncryption } from './naluUtils';
|
|
28
|
+
import { normalizeAnnexBStartCodes, processNALUsForEncryption } from './naluUtils';
|
|
29
29
|
import { identifySifPayload } from './sifPayload';
|
|
30
30
|
|
|
31
31
|
export const encryptionEnabledMap: Map<string, boolean> = new Map();
|
|
@@ -490,7 +490,16 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
490
490
|
let frameInfo = this.getUnencryptedBytes(encodedFrame);
|
|
491
491
|
|
|
492
492
|
// Thіs is not encrypted and contains the VP8 payload descriptor or the Opus TOC byte.
|
|
493
|
-
|
|
493
|
+
let frameHeader: NonSharedUint8Array = new Uint8Array(
|
|
494
|
+
encodedFrame.data,
|
|
495
|
+
0,
|
|
496
|
+
frameInfo.unencryptedBytes,
|
|
497
|
+
);
|
|
498
|
+
if (frameInfo.requiresNALUProcessing) {
|
|
499
|
+
// The receiver's depacketizer rebuilds NALUs with 4-byte start codes, so authenticate (and
|
|
500
|
+
// send) the header in that form or decryption fails for encoders emitting 3-byte ones.
|
|
501
|
+
frameHeader = normalizeAnnexBStartCodes(frameHeader);
|
|
502
|
+
}
|
|
494
503
|
|
|
495
504
|
// Frame trailer contains the R|IV_LENGTH and key index
|
|
496
505
|
const frameTrailer = new Uint8Array(2);
|
|
@@ -510,7 +519,7 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
510
519
|
{
|
|
511
520
|
name: ENCRYPTION_ALGORITHM,
|
|
512
521
|
iv,
|
|
513
|
-
additionalData:
|
|
522
|
+
additionalData: frameHeader,
|
|
514
523
|
},
|
|
515
524
|
encryptionKey,
|
|
516
525
|
new Uint8Array(encodedFrame.data, frameInfo.unencryptedBytes),
|
|
@@ -906,7 +915,7 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
906
915
|
};
|
|
907
916
|
}
|
|
908
917
|
} catch (e) {
|
|
909
|
-
this.logNALUFallbackOnce(fallbackKey, payloadType, e);
|
|
918
|
+
this.logNALUFallbackOnce(fallbackKey, frame, payloadType, detectedCodec, e);
|
|
910
919
|
}
|
|
911
920
|
|
|
912
921
|
// Fallback to VP8 handling
|
|
@@ -919,7 +928,9 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
919
928
|
*/
|
|
920
929
|
private logNALUFallbackOnce(
|
|
921
930
|
fallbackKey: string,
|
|
931
|
+
frame: RTCEncodedVideoFrame,
|
|
922
932
|
payloadType: number | undefined,
|
|
933
|
+
detectedCodec: VideoCodec | undefined,
|
|
923
934
|
error: unknown,
|
|
924
935
|
) {
|
|
925
936
|
if (this.loggedNALUFallbacks.has(fallbackKey)) {
|
|
@@ -929,6 +940,12 @@ export class FrameCryptor extends BaseFrameCryptor {
|
|
|
929
940
|
workerLogger.warn('NALU processing failed, falling back to VP8 handling', {
|
|
930
941
|
error,
|
|
931
942
|
payloadType,
|
|
943
|
+
detectedCodec,
|
|
944
|
+
frameType: frame.type,
|
|
945
|
+
byteLength: frame.data.byteLength,
|
|
946
|
+
firstBytes: Array.from(new Uint8Array(frame.data, 0, Math.min(16, frame.data.byteLength)))
|
|
947
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
948
|
+
.join(' '),
|
|
932
949
|
...this.logContext,
|
|
933
950
|
});
|
|
934
951
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { normalizeAnnexBStartCodes, processNALUsForEncryption } from './naluUtils';
|
|
3
|
+
|
|
4
|
+
/** Annex-B frame: a 4-byte start code before every NALU. */
|
|
5
|
+
function annexB(...nalus: number[][]) {
|
|
6
|
+
return Uint8Array.from(nalus.flatMap((nalu) => [0x00, 0x00, 0x00, 0x01, ...nalu]));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// h264: 0x06 SEI, 0x65 IDR slice, 0x67 SPS. h265: 0x26 IDR_W_RADL slice.
|
|
10
|
+
// An h264 SEI byte (0x06) reads as h265 TSA_R, which is why the SEI leads here.
|
|
11
|
+
// findNALUIndices stops at stream.length - 3. A trailing NALU with fewer than 4 payload
|
|
12
|
+
// bytes is never indexed, so its fixture tests nothing. Interior NALUs are fine at any
|
|
13
|
+
// length, because the next start code ends them.
|
|
14
|
+
const h264KeyFrame = annexB([0x06, 0x11, 0x22], [0x65, 0x33, 0x44, 0x55, 0x66]);
|
|
15
|
+
const h265KeyFrame = annexB([0x26, 0x11, 0x22], [0x02, 0x33, 0x44, 0x55, 0x66]);
|
|
16
|
+
const noSliceFrame = annexB([0x67, 0x11, 0x22, 0x33, 0x44, 0x55]);
|
|
17
|
+
// 0x25 is a slice under both: h264 &0x1f = 5 IDR, h265 (b >> 1) & 0x3f = 18 BLA_W_RADL.
|
|
18
|
+
const ambiguousFrame = annexB([0x25, 0x11, 0x22, 0x33, 0x44, 0x55]);
|
|
19
|
+
|
|
20
|
+
describe('normalizeAnnexBStartCodes', () => {
|
|
21
|
+
it('rewrites 3-byte start codes to 4-byte start codes', () => {
|
|
22
|
+
// prettier-ignore
|
|
23
|
+
const input = new Uint8Array([
|
|
24
|
+
0x00, 0x00, 0x01, 0x67, 0x64, // SPS
|
|
25
|
+
0x00, 0x00, 0x01, 0x68, 0xee, // PPS
|
|
26
|
+
0x00, 0x00, 0x01, 0x65, 0x88, // IDR slice
|
|
27
|
+
]);
|
|
28
|
+
// prettier-ignore
|
|
29
|
+
const expected = new Uint8Array([
|
|
30
|
+
0x00, 0x00, 0x00, 0x01, 0x67, 0x64,
|
|
31
|
+
0x00, 0x00, 0x00, 0x01, 0x68, 0xee,
|
|
32
|
+
0x00, 0x00, 0x00, 0x01, 0x65, 0x88,
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
expect(normalizeAnnexBStartCodes(input)).toEqual(expected);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('rewrites only the 3-byte start codes when mixed with 4-byte ones', () => {
|
|
39
|
+
// prettier-ignore
|
|
40
|
+
const input = new Uint8Array([
|
|
41
|
+
0x00, 0x00, 0x00, 0x01, 0x67, 0x64,
|
|
42
|
+
0x00, 0x00, 0x01, 0x68, 0xee,
|
|
43
|
+
0x00, 0x00, 0x00, 0x01, 0x06, 0x05,
|
|
44
|
+
0x00, 0x00, 0x01, 0x65, 0x88,
|
|
45
|
+
]);
|
|
46
|
+
// prettier-ignore
|
|
47
|
+
const expected = new Uint8Array([
|
|
48
|
+
0x00, 0x00, 0x00, 0x01, 0x67, 0x64,
|
|
49
|
+
0x00, 0x00, 0x00, 0x01, 0x68, 0xee,
|
|
50
|
+
0x00, 0x00, 0x00, 0x01, 0x06, 0x05,
|
|
51
|
+
0x00, 0x00, 0x00, 0x01, 0x65, 0x88,
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
expect(normalizeAnnexBStartCodes(input)).toEqual(expected);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns the same instance when all start codes are already 4-byte', () => {
|
|
58
|
+
// prettier-ignore
|
|
59
|
+
const input = new Uint8Array([
|
|
60
|
+
0x00, 0x00, 0x00, 0x01, 0x67, 0x64,
|
|
61
|
+
0x00, 0x00, 0x00, 0x01, 0x65, 0x88,
|
|
62
|
+
]);
|
|
63
|
+
|
|
64
|
+
expect(normalizeAnnexBStartCodes(input)).toBe(input);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does not add a zero when a start code already has extra leading zeros', () => {
|
|
68
|
+
const input = new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x01, 0x65, 0x88]);
|
|
69
|
+
|
|
70
|
+
expect(normalizeAnnexBStartCodes(input)).toBe(input);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('processNALUsForEncryption', () => {
|
|
75
|
+
it('slices at the h264 slice NALU even when seeded with h265', () => {
|
|
76
|
+
// The whole point: the h265 seed must not win, or the offset lands at 6 (inside the
|
|
77
|
+
// SEI) and the slice header gets encrypted with no error raised.
|
|
78
|
+
expect(processNALUsForEncryption(h264KeyFrame, 'h265')).toEqual({
|
|
79
|
+
unencryptedBytes: 13,
|
|
80
|
+
detectedCodec: 'h264',
|
|
81
|
+
requiresNALUProcessing: true,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('reads an h264 frame whose SEI precedes the slice as h264', () => {
|
|
86
|
+
expect(processNALUsForEncryption(h264KeyFrame)).toEqual({
|
|
87
|
+
unencryptedBytes: 13,
|
|
88
|
+
detectedCodec: 'h264',
|
|
89
|
+
requiresNALUProcessing: true,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('still reads an h265 frame as h265', () => {
|
|
94
|
+
expect(processNALUsForEncryption(h265KeyFrame)).toEqual({
|
|
95
|
+
unencryptedBytes: 6,
|
|
96
|
+
detectedCodec: 'h265',
|
|
97
|
+
requiresNALUProcessing: true,
|
|
98
|
+
});
|
|
99
|
+
expect(processNALUsForEncryption(h265KeyFrame, 'h264')).toEqual({
|
|
100
|
+
unencryptedBytes: 6,
|
|
101
|
+
detectedCodec: 'h265',
|
|
102
|
+
requiresNALUProcessing: true,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('resolves a byte that is a slice under both codecs as h264', () => {
|
|
107
|
+
// Only the h264-first ordering decides this one. Swap the two loops back and it reads h265.
|
|
108
|
+
expect(processNALUsForEncryption(ambiguousFrame)).toEqual({
|
|
109
|
+
unencryptedBytes: 6,
|
|
110
|
+
detectedCodec: 'h264',
|
|
111
|
+
requiresNALUProcessing: true,
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('throws with a seed, and returns quietly without one, when no slice NALU is present', () => {
|
|
116
|
+
expect(() => processNALUsForEncryption(noSliceFrame, 'h264')).toThrow('Could not find NALU');
|
|
117
|
+
expect(processNALUsForEncryption(noSliceFrame)).toEqual({
|
|
118
|
+
unencryptedBytes: 0,
|
|
119
|
+
detectedCodec: 'unknown',
|
|
120
|
+
requiresNALUProcessing: false,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -202,8 +202,14 @@ export interface NALUProcessingResult {
|
|
|
202
202
|
* @returns Detected codec type
|
|
203
203
|
*/
|
|
204
204
|
function detectCodecFromNALUs(data: NonSharedUint8Array, naluIndices: number[]): DetectedCodec {
|
|
205
|
+
// h264 runs to completion first. Several non-slice h264 NALUs read as h265 slice types
|
|
206
|
+
// (AUD 0x09 -> STSA_N, SEI 0x06 -> TSA_R), so interleaving the two checks lets one of
|
|
207
|
+
// those outrank the real h264 slice that follows it. The reverse cannot happen: h265
|
|
208
|
+
// header bytes are even at nuh_layer_id 0, so they never mask to an h264 slice type.
|
|
205
209
|
for (const naluIndex of naluIndices) {
|
|
206
210
|
if (isH264SliceNALU(parseH264NALUType(data[naluIndex]))) return 'h264';
|
|
211
|
+
}
|
|
212
|
+
for (const naluIndex of naluIndices) {
|
|
207
213
|
if (isH265SliceNALU(parseH265NALUType(data[naluIndex]))) return 'h265';
|
|
208
214
|
}
|
|
209
215
|
return 'unknown';
|
|
@@ -306,7 +312,7 @@ function findNALUIndices(stream: NonSharedUint8Array): number[] {
|
|
|
306
312
|
/**
|
|
307
313
|
* Process NALU data for frame encryption, detecting codec and finding unencrypted bytes
|
|
308
314
|
* @param data Frame data
|
|
309
|
-
* @param knownCodec
|
|
315
|
+
* @param knownCodec Codec from other sources, used only when the bytes are inconclusive (optional)
|
|
310
316
|
* @returns NALU processing result
|
|
311
317
|
*/
|
|
312
318
|
export function processNALUsForEncryption(
|
|
@@ -314,7 +320,16 @@ export function processNALUsForEncryption(
|
|
|
314
320
|
knownCodec?: 'h264' | 'h265',
|
|
315
321
|
): NALUProcessingResult {
|
|
316
322
|
const naluIndices = findNALUIndices(data);
|
|
317
|
-
|
|
323
|
+
// The bytes outrank knownCodec, which can be the codec the client asked for rather than the
|
|
324
|
+
// one that was negotiated. Parsing the wrong one mis-slices without raising anything: the
|
|
325
|
+
// offset lands before the real slice header, so the header itself gets encrypted.
|
|
326
|
+
// That holds in one direction only. An h264 access unit carrying no slice NALU but carrying
|
|
327
|
+
// an AUD or SEI that reads as an h265 slice now slices there instead of throwing. No real
|
|
328
|
+
// WebRTC access unit can reach it, because every one of them carries a type 1 or 5 slice.
|
|
329
|
+
let detectedCodec = detectCodecFromNALUs(data, naluIndices);
|
|
330
|
+
if (detectedCodec === 'unknown') {
|
|
331
|
+
detectedCodec = knownCodec ?? 'unknown';
|
|
332
|
+
}
|
|
318
333
|
|
|
319
334
|
if (detectedCodec === 'unknown') {
|
|
320
335
|
return { unencryptedBytes: 0, detectedCodec, requiresNALUProcessing: false };
|
|
@@ -327,3 +342,43 @@ export function processNALUsForEncryption(
|
|
|
327
342
|
|
|
328
343
|
return { unencryptedBytes, detectedCodec, requiresNALUProcessing: true };
|
|
329
344
|
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Rewrite every 3-byte Annex B start code (00 00 01) in `data` to the 4-byte form (00 00 00 01).
|
|
348
|
+
*
|
|
349
|
+
* WebRTC's H.264/H.265 depacketizer always reconstructs NALUs with 4-byte start codes, regardless
|
|
350
|
+
* of what the encoder emitted. Some encoders (e.g. Intel Quick Sync) emit 3-byte start codes, so
|
|
351
|
+
* the unencrypted header a receiver sees would differ from what the sender authenticated as
|
|
352
|
+
* AES-GCM additionalData. Normalizing on the sender makes both sides agree.
|
|
353
|
+
*
|
|
354
|
+
* Returns `data` unchanged (same instance) when it contains no 3-byte start codes.
|
|
355
|
+
*/
|
|
356
|
+
export function normalizeAnnexBStartCodes(data: NonSharedUint8Array): NonSharedUint8Array {
|
|
357
|
+
const shortStartCodes: number[] = [];
|
|
358
|
+
for (let i = 0; i + 2 < data.length;) {
|
|
359
|
+
if (data[i + 2] === 0) {
|
|
360
|
+
i += 1;
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (data[i + 2] === 1 && data[i] === 0 && data[i + 1] === 0 && (i === 0 || data[i - 1] !== 0)) {
|
|
364
|
+
shortStartCodes.push(i);
|
|
365
|
+
}
|
|
366
|
+
// data[i + 2] is non-zero, so no start code can begin at i + 1 or i + 2 either.
|
|
367
|
+
i += 3;
|
|
368
|
+
}
|
|
369
|
+
if (shortStartCodes.length === 0) {
|
|
370
|
+
return data;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// The new array is zero-filled, so the one-byte gap left before each short start code becomes
|
|
374
|
+
// the inserted leading 00.
|
|
375
|
+
const result = new Uint8Array(data.length + shortStartCodes.length);
|
|
376
|
+
let from = 0;
|
|
377
|
+
for (let inserted = 0; inserted < shortStartCodes.length; inserted++) {
|
|
378
|
+
const to = shortStartCodes[inserted];
|
|
379
|
+
result.set(data.subarray(from, to), from + inserted);
|
|
380
|
+
from = to;
|
|
381
|
+
}
|
|
382
|
+
result.set(data.subarray(from), from + shortStartCodes.length);
|
|
383
|
+
return result;
|
|
384
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
isSVCCodec,
|
|
57
57
|
isVideoCodec,
|
|
58
58
|
isVideoTrack,
|
|
59
|
+
releaseEmptyAudioStreamTrack,
|
|
59
60
|
supportsAV1,
|
|
60
61
|
supportsAdaptiveStream,
|
|
61
62
|
supportsAudioOutputSelection,
|
|
@@ -145,6 +146,7 @@ export {
|
|
|
145
146
|
getEmptyVideoStreamTrack,
|
|
146
147
|
getLogger,
|
|
147
148
|
isBrowserSupported,
|
|
149
|
+
releaseEmptyAudioStreamTrack,
|
|
148
150
|
setLogExtension,
|
|
149
151
|
setLogLevel,
|
|
150
152
|
supportsAV1,
|
package/src/options.ts
CHANGED
|
@@ -166,6 +166,22 @@ export interface InternalRoomConnectOptions {
|
|
|
166
166
|
*/
|
|
167
167
|
rtcConfig?: RTCConfiguration;
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* when the server runs ICE lite, asks it to use a full ICE agent for this participant.
|
|
171
|
+
* this is a server-side setting that the server may ignore, defaults to false
|
|
172
|
+
*/
|
|
173
|
+
disableIceLite: boolean;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* whether the SFU may pause subscribed video tracks when this participant's downstream
|
|
177
|
+
* connection is congested, so that other tracks can continue to stream smoothly.
|
|
178
|
+
* paused tracks resume automatically when conditions allow, and `RoomEvent.TrackStreamStateChanged`
|
|
179
|
+
* fires on each transition.
|
|
180
|
+
* when left undefined, the server's default applies (the project setting on LiveKit Cloud,
|
|
181
|
+
* or `rtc.congestion_control.allow_pause` when self-hosting)
|
|
182
|
+
*/
|
|
183
|
+
subscriberAllowPause?: boolean;
|
|
184
|
+
|
|
169
185
|
/** specifies how often an initial join connection is allowed to retry (only applicable if server is not reachable) */
|
|
170
186
|
maxRetries: number;
|
|
171
187
|
|