livekit-client 2.18.8 → 2.18.10
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 +2898 -2431
- 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 -1
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +3 -1
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-track/LocalDataTrack.d.ts +2 -1
- package/dist/src/room/data-track/LocalDataTrack.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/OutgoingDataTrackManager.d.ts +2 -2
- package/dist/src/room/data-track/outgoing/OutgoingDataTrackManager.d.ts.map +1 -1
- package/dist/src/room/data-track/outgoing/types.d.ts +4 -3
- 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 +3 -1
- 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/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 +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 -1
- package/dist/ts4.2/room/Room.d.ts +3 -1
- package/dist/ts4.2/room/data-track/LocalDataTrack.d.ts +2 -1
- 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/OutgoingDataTrackManager.d.ts +2 -2
- package/dist/ts4.2/room/data-track/outgoing/types.d.ts +4 -3
- 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 +3 -1
- package/dist/ts4.2/room/participant/Participant.d.ts +1 -1
- 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 +1 -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 -20
- package/src/room/Room.test.ts +62 -1
- package/src/room/Room.ts +28 -5
- package/src/room/data-track/LocalDataTrack.ts +15 -7
- 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/OutgoingDataTrackManager.test.ts +350 -198
- package/src/room/data-track/outgoing/OutgoingDataTrackManager.ts +9 -3
- package/src/room/data-track/outgoing/types.ts +4 -3
- 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 +48 -7
- package/src/room/participant/Participant.ts +1 -1
- package/src/room/participant/publishUtils.ts +1 -1
- 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 +17 -3
- 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 +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { TrackInfo } from '@livekit/protocol';
|
|
2
|
+
import log from '../logger';
|
|
3
|
+
import type Room from '../room/Room';
|
|
4
|
+
import { RoomEvent } from '../room/events';
|
|
5
|
+
import { PacketTrailerExtractor } from '../room/track/PacketTrailerExtractor';
|
|
6
|
+
import type RemoteTrack from '../room/track/RemoteTrack';
|
|
7
|
+
import RemoteVideoTrack from '../room/track/RemoteVideoTrack';
|
|
8
|
+
import type { PTDecodeMessage, PTUpdateTrackIdMessage, PTWorkerMessage } from './types';
|
|
9
|
+
import { isPacketTrailerSupported, shouldUsePacketTrailerScriptTransform } from './utils';
|
|
10
|
+
|
|
11
|
+
export interface PacketTrailerOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Dedicated worker for extracting packet trailers off the main thread.
|
|
14
|
+
*
|
|
15
|
+
* Encoded video streams are transferred to the worker for processing, which
|
|
16
|
+
* avoids per-frame work on the main thread.
|
|
17
|
+
*/
|
|
18
|
+
worker: Worker;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Manages packet trailer extraction for received video tracks.
|
|
23
|
+
*
|
|
24
|
+
* When a track's TrackInfo indicates packet trailer features, the manager
|
|
25
|
+
* wires up an encoded frame transform to strip the trailer from encoded frames
|
|
26
|
+
* and cache the metadata for lookup.
|
|
27
|
+
*
|
|
28
|
+
* Packet trailer extraction is worker-only. If no worker is configured, the
|
|
29
|
+
* SDK does not advertise packet trailer support and skips extraction.
|
|
30
|
+
*
|
|
31
|
+
* When E2EE is active, the E2EE FrameCryptor worker handles trailer
|
|
32
|
+
* extraction directly (before decryption), so this manager only creates
|
|
33
|
+
* the extractor/metadata cache — no separate pipeline is installed.
|
|
34
|
+
*
|
|
35
|
+
* @experimental
|
|
36
|
+
*/
|
|
37
|
+
export class PacketTrailerManager {
|
|
38
|
+
private worker?: Worker;
|
|
39
|
+
|
|
40
|
+
private room?: Room;
|
|
41
|
+
|
|
42
|
+
private extractors = new Map<string, PacketTrailerExtractor>();
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Tracks the trackId associated with each receiver that has had its
|
|
46
|
+
* encoded streams handed off to the worker. Used to detect receiver
|
|
47
|
+
* reuse (transceiver recycling) so we can remap trackIds instead of
|
|
48
|
+
* re-transferring already-consumed streams.
|
|
49
|
+
*/
|
|
50
|
+
private workerPipelines = new Map<RTCRtpReceiver, string>();
|
|
51
|
+
|
|
52
|
+
constructor(options?: PacketTrailerOptions) {
|
|
53
|
+
this.worker = options?.worker;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @internal */
|
|
57
|
+
setup(room: Room) {
|
|
58
|
+
if (room === this.room) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.room = room;
|
|
62
|
+
|
|
63
|
+
if (this.worker) {
|
|
64
|
+
this.worker.onmessage = this.onWorkerMessage;
|
|
65
|
+
this.worker.onerror = this.onWorkerError;
|
|
66
|
+
this.worker.postMessage({ kind: 'init' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
room
|
|
70
|
+
.on(RoomEvent.TrackSubscribed, (track, pub, _participant) => {
|
|
71
|
+
if (track.kind !== 'video') {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
this.setupReceiver(track as unknown as RemoteVideoTrack, pub.trackInfo);
|
|
75
|
+
})
|
|
76
|
+
.on(RoomEvent.TrackUnsubscribed, (track) => {
|
|
77
|
+
this.teardownTrack(track);
|
|
78
|
+
})
|
|
79
|
+
.on(RoomEvent.Disconnected, () => {
|
|
80
|
+
this.cleanup();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private setupReceiver(track: RemoteVideoTrack, trackInfo?: TrackInfo) {
|
|
85
|
+
const receiver = track.receiver;
|
|
86
|
+
if (!receiver) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Only install a pipeline for tracks that actually advertise packet
|
|
91
|
+
// trailer features. This keeps us out of the way for tracks published by
|
|
92
|
+
// clients on older protocols or that don't opt into the feature.
|
|
93
|
+
const hasFeatures =
|
|
94
|
+
!!trackInfo?.packetTrailerFeatures && trackInfo.packetTrailerFeatures.length > 0;
|
|
95
|
+
if (!hasFeatures) {
|
|
96
|
+
if (!this.room?.hasE2EESetup) {
|
|
97
|
+
this.setupPassthroughReceiver(receiver, track.mediaStreamID);
|
|
98
|
+
}
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (
|
|
103
|
+
!isPacketTrailerSupported(this.worker ? { worker: this.worker } : undefined) &&
|
|
104
|
+
!this.room?.hasE2EESetup
|
|
105
|
+
) {
|
|
106
|
+
log.warn('packet trailer transform not supported; skipping extraction');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const extractor = new PacketTrailerExtractor();
|
|
111
|
+
const trackId = track.mediaStreamID;
|
|
112
|
+
|
|
113
|
+
this.extractors.set(trackId, extractor);
|
|
114
|
+
track.packetTrailerExtractor = extractor;
|
|
115
|
+
|
|
116
|
+
if (this.room?.hasE2EESetup) {
|
|
117
|
+
// E2EE worker strips the trailer and injects metadata directly into
|
|
118
|
+
// the extractor via E2eeManager; no pipeline is needed here.
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
this.setupWorkerReceiver(receiver, trackId, true);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private setupPassthroughReceiver(receiver: RTCRtpReceiver, trackId: string) {
|
|
126
|
+
if (shouldUsePacketTrailerScriptTransform()) {
|
|
127
|
+
if ('transform' in receiver) {
|
|
128
|
+
// @ts-ignore
|
|
129
|
+
receiver.transform = null;
|
|
130
|
+
}
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
this.worker &&
|
|
136
|
+
isPacketTrailerSupported({ worker: this.worker }) &&
|
|
137
|
+
!this.workerPipelines.has(receiver)
|
|
138
|
+
) {
|
|
139
|
+
this.setupWorkerReceiver(receiver, trackId, false);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (this.worker && this.workerPipelines.has(receiver)) {
|
|
144
|
+
this.setupWorkerReceiver(receiver, trackId, false);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private setupWorkerReceiver(
|
|
149
|
+
receiver: RTCRtpReceiver,
|
|
150
|
+
newTrackId: string,
|
|
151
|
+
hasPacketTrailer = true,
|
|
152
|
+
) {
|
|
153
|
+
const worker = this.worker;
|
|
154
|
+
if (!worker) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (shouldUsePacketTrailerScriptTransform()) {
|
|
159
|
+
// @ts-ignore
|
|
160
|
+
receiver.transform = new RTCRtpScriptTransform(worker, {
|
|
161
|
+
kind: 'decode',
|
|
162
|
+
trackId: newTrackId,
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const existingTrackId = this.workerPipelines.get(receiver);
|
|
168
|
+
|
|
169
|
+
if (existingTrackId) {
|
|
170
|
+
// Receiver is reused (transceiver recycled). The worker already owns
|
|
171
|
+
// the encoded streams — just remap the trackId so metadata is keyed
|
|
172
|
+
// correctly and re-activate processing.
|
|
173
|
+
const msg: PTUpdateTrackIdMessage = {
|
|
174
|
+
kind: 'updateTrackId',
|
|
175
|
+
data: { oldTrackId: existingTrackId, newTrackId, hasPacketTrailer },
|
|
176
|
+
};
|
|
177
|
+
worker.postMessage(msg);
|
|
178
|
+
this.workerPipelines.set(receiver, newTrackId);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!('createEncodedStreams' in receiver)) {
|
|
183
|
+
log.warn('createEncodedStreams not supported');
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let streams: { readable: ReadableStream; writable: WritableStream };
|
|
188
|
+
try {
|
|
189
|
+
// @ts-ignore — createEncodedStreams is not in standard typings
|
|
190
|
+
streams = receiver.createEncodedStreams();
|
|
191
|
+
} catch (err) {
|
|
192
|
+
log.warn('failed to create encoded streams', { error: err });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const msg: PTDecodeMessage = {
|
|
197
|
+
kind: 'decode',
|
|
198
|
+
data: {
|
|
199
|
+
readableStream: streams.readable,
|
|
200
|
+
writableStream: streams.writable,
|
|
201
|
+
trackId: newTrackId,
|
|
202
|
+
hasPacketTrailer,
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
worker.postMessage(msg, [streams.readable, streams.writable]);
|
|
206
|
+
this.workerPipelines.set(receiver, newTrackId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private teardownTrack(track: RemoteTrack) {
|
|
210
|
+
const trackId = track.mediaStreamID;
|
|
211
|
+
const extractor = this.extractors.get(trackId);
|
|
212
|
+
if (extractor) {
|
|
213
|
+
extractor.dispose();
|
|
214
|
+
this.extractors.delete(trackId);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (track instanceof RemoteVideoTrack) {
|
|
218
|
+
track.packetTrailerExtractor = undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// The receiver pipeline is intentionally left running. If the receiver is
|
|
222
|
+
// reused for a new track, `setupReceiver` will remap it. If the room
|
|
223
|
+
// disconnects, `cleanup` drops all state. Any metadata produced in the
|
|
224
|
+
// meantime is harmless — the extractor above has already been disposed and
|
|
225
|
+
// is no longer reachable from any track.
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private cleanup() {
|
|
229
|
+
for (const extractor of this.extractors.values()) {
|
|
230
|
+
extractor.dispose();
|
|
231
|
+
}
|
|
232
|
+
this.extractors.clear();
|
|
233
|
+
this.workerPipelines.clear();
|
|
234
|
+
this.worker?.terminate();
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private onWorkerMessage = (ev: MessageEvent<PTWorkerMessage>) => {
|
|
238
|
+
const msg = ev.data;
|
|
239
|
+
if (msg.kind === 'metadata') {
|
|
240
|
+
const extractor = this.extractors.get(msg.data.trackId);
|
|
241
|
+
if (extractor) {
|
|
242
|
+
extractor.storeMetadata(msg.data.rtpTimestamp, msg.data.ssrc, msg.data.metadata);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
private onWorkerError = (ev: ErrorEvent) => {
|
|
248
|
+
log.error('packet trailer worker encountered an error:', { error: ev.error });
|
|
249
|
+
};
|
|
250
|
+
}
|
|
@@ -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
|
+
}
|