@gjsify/webrtc 0.4.0 → 0.4.4

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.
Files changed (44) hide show
  1. package/package.json +73 -70
  2. package/src/get-user-media.ts +0 -131
  3. package/src/gst-enum-maps.ts +0 -125
  4. package/src/gst-init.ts +0 -49
  5. package/src/gst-stats-parser.ts +0 -137
  6. package/src/gst-utils.ts +0 -41
  7. package/src/index.ts +0 -104
  8. package/src/internal/gst-types.ts +0 -122
  9. package/src/media-device-info.ts +0 -33
  10. package/src/media-devices.ts +0 -191
  11. package/src/media-stream-track.ts +0 -159
  12. package/src/media-stream.ts +0 -96
  13. package/src/register/data-channel.ts +0 -11
  14. package/src/register/error.ts +0 -11
  15. package/src/register/media-devices.ts +0 -10
  16. package/src/register/media.ts +0 -15
  17. package/src/register/peer-connection.ts +0 -20
  18. package/src/register.spec.ts +0 -55
  19. package/src/register.ts +0 -10
  20. package/src/rtc-certificate.ts +0 -110
  21. package/src/rtc-data-channel.ts +0 -283
  22. package/src/rtc-dtls-transport.ts +0 -48
  23. package/src/rtc-dtmf-sender.ts +0 -146
  24. package/src/rtc-error.ts +0 -49
  25. package/src/rtc-events.ts +0 -64
  26. package/src/rtc-ice-candidate.ts +0 -115
  27. package/src/rtc-ice-transport.ts +0 -104
  28. package/src/rtc-peer-connection.ts +0 -1039
  29. package/src/rtc-rtp-receiver.ts +0 -122
  30. package/src/rtc-rtp-sender.ts +0 -471
  31. package/src/rtc-rtp-transceiver.ts +0 -131
  32. package/src/rtc-sctp-transport.ts +0 -48
  33. package/src/rtc-session-description.ts +0 -64
  34. package/src/rtc-stats-report.ts +0 -39
  35. package/src/rtc-track-event.ts +0 -45
  36. package/src/rtp-capabilities.ts +0 -48
  37. package/src/tee-multiplexer.ts +0 -75
  38. package/src/test.mts +0 -11
  39. package/src/webrtc.spec.ts +0 -1186
  40. package/src/wpt-helpers.ts +0 -156
  41. package/src/wpt-media.spec.ts +0 -1154
  42. package/src/wpt.spec.ts +0 -1136
  43. package/tsconfig.json +0 -36
  44. package/tsconfig.tsbuildinfo +0 -1
@@ -1,122 +0,0 @@
1
- // W3C RTCRtpReceiver for GJS.
2
- //
3
- // Wraps GstWebRTC.WebRTCRTPReceiver. Phase 2.5: incoming media pipeline
4
- // is managed by ReceiverBridge (Vala) which handles decodebin's
5
- // streaming-thread signals natively and emits media-flowing on the
6
- // main thread when decoded media replaces the muted source.
7
- //
8
- // Reference: refs/node-gst-webrtc/src/webrtc/RTCRtpReceiver.ts (ISC)
9
- // Reference: W3C WebRTC spec § 5.3
10
-
11
- import type GstWebRTC from 'gi://GstWebRTC?version=1.0';
12
- import type Gst from 'gi://Gst?version=1.0';
13
-
14
- import {
15
- ReceiverBridge,
16
- type ReceiverBridge as ReceiverBridgeType,
17
- } from '@gjsify/webrtc-native';
18
-
19
- import { MediaStreamTrack } from './media-stream-track.js';
20
- import { getRtpCapabilities } from './rtp-capabilities.js';
21
- import type { RTCStatsReport } from './rtc-stats-report.js';
22
- import type { RTCDtlsTransport } from './rtc-dtls-transport.js';
23
- import type { RTCRtpCapabilities, RTCRtpCodecParameters, RTCRtpHeaderExtensionParameters, RTCRtcpParameters } from './rtc-rtp-sender.js';
24
-
25
- export interface RTCRtpReceiveParameters {
26
- codecs: RTCRtpCodecParameters[];
27
- headerExtensions: RTCRtpHeaderExtensionParameters[];
28
- rtcp: RTCRtcpParameters;
29
- }
30
-
31
- export interface RTCRtpContributingSource {
32
- timestamp: number;
33
- source: number;
34
- audioLevel?: number;
35
- rtpTimestamp: number;
36
- }
37
-
38
- export interface RTCRtpSynchronizationSource extends RTCRtpContributingSource {
39
- voiceActivityFlag?: boolean;
40
- }
41
-
42
- const MAX_JITTER_BUFFER_TARGET = 4000;
43
-
44
- export class RTCRtpReceiver {
45
- private _gstReceiver: GstWebRTC.WebRTCRTPReceiver | null;
46
- private _track: MediaStreamTrack;
47
- private _jitterBufferTarget: number | null = null;
48
- private _pipeline: any = null;
49
- private _receiverBridge: ReceiverBridgeType | null = null;
50
- /** @internal — stats callback set by RTCPeerConnection */
51
- _getStatsForTrack: ((track: MediaStreamTrack) => Promise<RTCStatsReport>) | null = null;
52
-
53
- constructor(kind: 'audio' | 'video', gstReceiver: GstWebRTC.WebRTCRTPReceiver | null, pipeline?: any) {
54
- this._gstReceiver = gstReceiver;
55
- this._pipeline = pipeline ?? null;
56
- this._track = new MediaStreamTrack({ kind, muted: true });
57
- }
58
-
59
- /** @internal — called from RTCPeerConnection._handlePadAdded */
60
- _connectToPad(pad: Gst.Pad): void {
61
- if (!this._pipeline || this._receiverBridge) return;
62
- this._receiverBridge = new (ReceiverBridge as any)({
63
- pipeline: this._pipeline,
64
- kind: this._track.kind,
65
- });
66
- this._receiverBridge!.connect_to_pad(pad);
67
- this._receiverBridge!.connect('media-flowing', () => {
68
- this._track._setMuted(false);
69
- });
70
- }
71
-
72
- /** @internal — called from RTCPeerConnection.close() */
73
- _dispose(): void {
74
- try { this._receiverBridge?.dispose_bridge(); } catch { /* ignore */ }
75
- this._receiverBridge = null;
76
- }
77
-
78
- /** @internal — set by RTCPeerConnection */
79
- _transport: RTCDtlsTransport | null = null;
80
-
81
- get track(): MediaStreamTrack { return this._track; }
82
- get transport(): RTCDtlsTransport | null { return this._transport; }
83
-
84
- get jitterBufferTarget(): number | null { return this._jitterBufferTarget; }
85
- set jitterBufferTarget(v: number | null) {
86
- if (v === null) {
87
- this._jitterBufferTarget = null;
88
- return;
89
- }
90
- const n = Number(v);
91
- if (!Number.isFinite(n) || n < 0) {
92
- throw new RangeError(`Failed to set jitterBufferTarget: ${v} is negative or not finite`);
93
- }
94
- if (n > MAX_JITTER_BUFFER_TARGET) {
95
- throw new RangeError(`Failed to set jitterBufferTarget: ${v} exceeds maximum of ${MAX_JITTER_BUFFER_TARGET}`);
96
- }
97
- this._jitterBufferTarget = n;
98
- }
99
-
100
- getParameters(): RTCRtpReceiveParameters {
101
- return {
102
- codecs: [],
103
- headerExtensions: [],
104
- rtcp: {},
105
- };
106
- }
107
-
108
- getContributingSources(): RTCRtpContributingSource[] { return []; }
109
- getSynchronizationSources(): RTCRtpSynchronizationSource[] { return []; }
110
-
111
- async getStats(): Promise<RTCStatsReport> {
112
- if (this._getStatsForTrack && this._track) {
113
- return this._getStatsForTrack(this._track);
114
- }
115
- const { RTCStatsReport: Report } = await import('./rtc-stats-report.js');
116
- return new Report();
117
- }
118
-
119
- static getCapabilities(kind: string): RTCRtpCapabilities | null {
120
- return getRtpCapabilities(kind);
121
- }
122
- }
@@ -1,471 +0,0 @@
1
- // W3C RTCRtpSender for GJS.
2
- //
3
- // Phase 2: API surface wrapping GstWebRTC.WebRTCRTPSender.
4
- // Phase 3: outgoing media pipeline — builds explicit encoder chains
5
- // (source → valve → convert → encode → payloader → webrtcbin sink pad)
6
- // entirely on the main thread. No Vala bridge needed.
7
- //
8
- // Reference: refs/node-gst-webrtc/src/webrtc/RTCRtpSender.ts (ISC)
9
- // Reference: W3C WebRTC spec § 5.2
10
-
11
- import type GstNs from 'gi://Gst?version=1.0';
12
- import type GstWebRTC from 'gi://GstWebRTC?version=1.0';
13
-
14
- import { Gst } from './gst-init.js';
15
- import { getRtpCapabilities } from './rtp-capabilities.js';
16
- import { RTCDTMFSender } from './rtc-dtmf-sender.js';
17
- import { TeeMultiplexer } from './tee-multiplexer.js';
18
- import {
19
- asValveElement,
20
- asRtpPayloaderElement,
21
- asCapsFilterElement,
22
- asVp8EncElement,
23
- type ValveElement,
24
- } from './internal/gst-types.js';
25
- import type { RTCStatsReport } from './rtc-stats-report.js';
26
- import type { RTCDtlsTransport } from './rtc-dtls-transport.js';
27
- import type { MediaStreamTrack } from './media-stream-track.js';
28
- import type { MediaStream } from './media-stream.js';
29
-
30
- // Standard RTP payload types used in WebRTC SDP
31
- const OPUS_PAYLOAD_TYPE = 111;
32
- const VP8_PAYLOAD_TYPE = 96;
33
-
34
- export type RTCRtpTransceiverDirection = 'sendrecv' | 'sendonly' | 'recvonly' | 'inactive' | 'stopped';
35
-
36
- export interface RTCRtpCodecCapability {
37
- mimeType: string;
38
- clockRate: number;
39
- channels?: number;
40
- sdpFmtpLine?: string;
41
- }
42
-
43
- export interface RTCRtpHeaderExtensionCapability {
44
- uri: string;
45
- }
46
-
47
- export interface RTCRtpCapabilities {
48
- codecs: RTCRtpCodecCapability[];
49
- headerExtensions: RTCRtpHeaderExtensionCapability[];
50
- }
51
-
52
- export interface RTCRtpEncodingParameters {
53
- rid?: string;
54
- active?: boolean;
55
- maxBitrate?: number;
56
- maxFramerate?: number;
57
- scaleResolutionDownBy?: number;
58
- }
59
-
60
- export interface RTCRtpCodecParameters {
61
- payloadType: number;
62
- mimeType: string;
63
- clockRate: number;
64
- channels?: number;
65
- sdpFmtpLine?: string;
66
- }
67
-
68
- export interface RTCRtpHeaderExtensionParameters {
69
- uri: string;
70
- id: number;
71
- encrypted?: boolean;
72
- }
73
-
74
- export interface RTCRtcpParameters {
75
- cname?: string;
76
- reducedSize?: boolean;
77
- }
78
-
79
- export interface RTCRtpSendParameters {
80
- transactionId: string;
81
- encodings: RTCRtpEncodingParameters[];
82
- codecs: RTCRtpCodecParameters[];
83
- headerExtensions: RTCRtpHeaderExtensionParameters[];
84
- rtcp: RTCRtcpParameters;
85
- }
86
-
87
- let _txCounter = 0;
88
-
89
- export class RTCRtpSender {
90
- private _gstSender: GstWebRTC.WebRTCRTPSender | null;
91
- private _track: MediaStreamTrack | null = null;
92
- private _lastParams: RTCRtpSendParameters | null = null;
93
-
94
- /** @internal GStreamer pipeline references (set by RTCPeerConnection) */
95
- private _pipeline: GstNs.Pipeline | null = null;
96
- private _webrtcbin: GstNs.Element | null = null;
97
- private _mlineIndex: number = -1;
98
- private _elements: GstNs.Element[] = [];
99
- private _valve: ValveElement | null = null;
100
- _linked = false;
101
- /** @internal — tee src pad if this sender uses a shared source */
102
- private _teeSrcPad: GstNs.Pad | null = null;
103
- /** @internal — stats callback set by RTCPeerConnection */
104
- _getStatsForTrack: ((track: MediaStreamTrack) => Promise<RTCStatsReport>) | null = null;
105
- /** @internal — set by RTCPeerConnection */
106
- _transport: RTCDtlsTransport | null = null;
107
- /** @internal — DTMF sender, created lazily for audio senders */
108
- private _dtmf: RTCDTMFSender | null = null;
109
- /** @internal — the kind of media this sender handles */
110
- _kind: 'audio' | 'video' | null = null;
111
- /** @internal — back-reference for DTMF stopped/direction checks */
112
- _transceiver: { stopped: boolean; currentDirection: string | null } | null = null;
113
- /** @internal — callback to notify RTCPeerConnection when pipeline changes (cross-pipeline fix) */
114
- _onPipelineChanged: ((newPipeline: GstNs.Pipeline) => void) | null = null;
115
-
116
- constructor(
117
- gstSender: GstWebRTC.WebRTCRTPSender | null,
118
- pipeline?: GstNs.Pipeline,
119
- webrtcbin?: GstNs.Element,
120
- ) {
121
- this._gstSender = gstSender;
122
- this._pipeline = pipeline ?? null;
123
- this._webrtcbin = webrtcbin ?? null;
124
- }
125
-
126
- get track(): MediaStreamTrack | null { return this._track; }
127
-
128
- /** Returns the DTMF sender for audio senders, null for video. */
129
- get dtmf(): RTCDTMFSender | null {
130
- // Determine kind from track or from what was set by PC
131
- const kind = this._track?.kind ?? this._kind;
132
- if (kind !== 'audio') return null;
133
- if (!this._dtmf) {
134
- const dtmf = new RTCDTMFSender();
135
- // Wire transceiver state checks for insertDTMF validation
136
- dtmf._isStopped = () => this._transceiver?.stopped ?? false;
137
- dtmf._getCurrentDirection = () => this._transceiver?.currentDirection ?? null;
138
- this._dtmf = dtmf;
139
- }
140
- return this._dtmf;
141
- }
142
- get transport(): RTCDtlsTransport | null { return this._transport; }
143
-
144
- /** @internal */
145
- _setTrack(track: MediaStreamTrack | null): void {
146
- if (track === null && this._linked) {
147
- this._teardownPipeline();
148
- }
149
- this._track = track;
150
- }
151
-
152
- /** @internal — called by RTCPeerConnection._createTransceiverWrapper */
153
- _setMlineIndex(index: number): void { this._mlineIndex = index; }
154
-
155
- /** @internal — build the outgoing encoder chain and link to webrtcbin */
156
- _wirePipeline(track: MediaStreamTrack): void {
157
- if (this._linked || !this._pipeline || !this._webrtcbin) return;
158
- const source = track._gstSource as GstNs.Element | null;
159
- if (!source) return; // No GStreamer backing — nothing to wire
160
-
161
- // MediaStreamTrack's GStreamer-backing fields are intentionally typed
162
- // `any` on the track itself (they are runtime-attached by the
163
- // get-user-media / VideoBridge code paths). We narrow them locally
164
- // here to keep the rest of this method strongly typed.
165
- const trackGst = track as MediaStreamTrack & {
166
- _gstSource: GstNs.Element | null;
167
- _gstPipeline: GstNs.Pipeline | null;
168
- _gstTee: GstNs.Element | null;
169
- _teeMultiplexer: TeeMultiplexer | null;
170
- };
171
- let sourceForChain: GstNs.Element | null; // source directly or null when tee branch
172
-
173
- if (trackGst._gstTee && trackGst._gstPipeline && trackGst._gstPipeline !== this._pipeline) {
174
- // Source has a tee from VideoBridge (preview) in a different pipeline.
175
- // Instead of moving the source, we add our encoder chain elements to
176
- // the SOURCE's pipeline and request a new branch from its tee.
177
- // The webrtcbin must also move to the source pipeline.
178
- const sourcePipeline = trackGst._gstPipeline;
179
- const tee = trackGst._gstTee;
180
-
181
- // Move webrtcbin to the source pipeline so everything is in one pipeline
182
- if (this._webrtcbin.get_parent() === this._pipeline) {
183
- this._pipeline.set_state(Gst.State.NULL);
184
- this._pipeline.remove(this._webrtcbin);
185
- }
186
- sourcePipeline.add(this._webrtcbin);
187
- this._webrtcbin.sync_state_with_parent();
188
-
189
- // Switch our pipeline reference to the source's pipeline
190
- this._pipeline = sourcePipeline;
191
- // Notify the owning RTCPeerConnection to update its pipeline reference
192
- this._onPipelineChanged?.(sourcePipeline);
193
-
194
- // Request a new branch from the existing tee
195
- const teeSrcPad = tee.request_pad_simple
196
- ? tee.request_pad_simple('src_%u')
197
- : tee.get_request_pad('src_%u');
198
- this._teeSrcPad = teeSrcPad;
199
- sourceForChain = null; // We'll link via pad below
200
- } else if (trackGst._teeMultiplexer) {
201
- // Track already has a TeeMultiplexer (shared with another PC) — request a new branch
202
- const tee = trackGst._teeMultiplexer as TeeMultiplexer;
203
- const teeSrcPad = tee.requestSrcPad();
204
- this._teeSrcPad = teeSrcPad;
205
- sourceForChain = null; // We'll link via pad below
206
- } else if (trackGst._gstPipeline && trackGst._gstPipeline !== this._pipeline) {
207
- // Source is in another pipeline (no tee from VideoBridge) — this is the
208
- // second PC using this track. Insert a tee between source and the
209
- // existing consumer. Move source to this pipeline and create a tee.
210
- const oldPipeline = trackGst._gstPipeline;
211
-
212
- // First, unlink source from its current peer (the first sender's valve)
213
- const sourceSrcPad = source.get_static_pad('src');
214
- const oldPeer = sourceSrcPad?.get_peer?.();
215
- if (oldPeer) sourceSrcPad.unlink(oldPeer);
216
-
217
- source.set_state(Gst.State.NULL);
218
- oldPipeline.remove(source);
219
- this._pipeline.add(source);
220
- trackGst._gstPipeline = this._pipeline;
221
-
222
- // Create tee in this pipeline
223
- const tee = new TeeMultiplexer(this._pipeline, source);
224
- trackGst._teeMultiplexer = tee;
225
-
226
- // Reconnect the old consumer (first sender) via a tee branch
227
- if (oldPeer) {
228
- const firstBranch = tee.requestSrcPad();
229
- if (firstBranch) firstBranch.link(oldPeer);
230
- }
231
-
232
- // Request a branch for this sender
233
- const teeSrcPad = tee.requestSrcPad();
234
- this._teeSrcPad = teeSrcPad;
235
- sourceForChain = null;
236
- } else {
237
- // First PC to use this track — move source directly
238
- const oldPipeline = trackGst._gstPipeline;
239
- if (oldPipeline && oldPipeline !== this._pipeline) {
240
- source.set_state(Gst.State.NULL);
241
- oldPipeline.remove(source);
242
- trackGst._gstPipeline = this._pipeline;
243
- }
244
- if (source.get_parent() !== this._pipeline) {
245
- this._pipeline.add(source);
246
- }
247
- sourceForChain = source;
248
- }
249
-
250
- // Valve element for Track.enabled control
251
- const valve = asValveElement(Gst.ElementFactory.make('valve', null)!);
252
- valve.drop = !track.enabled;
253
- this._valve = valve;
254
- this._pipeline.add(valve);
255
-
256
- const elements: GstNs.Element[] = [valve];
257
- let lastElement: GstNs.Element;
258
-
259
- if (track.kind === 'audio') {
260
- const convert = Gst.ElementFactory.make('audioconvert', null)!;
261
- const resample = Gst.ElementFactory.make('audioresample', null)!;
262
- const encoder = Gst.ElementFactory.make('opusenc', null)!;
263
- const payloader = asRtpPayloaderElement(Gst.ElementFactory.make('rtpopuspay', null)!);
264
- payloader.pt = OPUS_PAYLOAD_TYPE;
265
-
266
- // capsfilter tells webrtcbin the RTP caps immediately so createOffer
267
- // can generate the m=audio line without waiting for data to flow.
268
- const capsfilter = asCapsFilterElement(Gst.ElementFactory.make('capsfilter', null)!);
269
- capsfilter.caps = Gst.Caps.from_string(
270
- `application/x-rtp,media=audio,encoding-name=OPUS,clock-rate=48000,payload=${OPUS_PAYLOAD_TYPE}`,
271
- );
272
-
273
- elements.push(convert, resample, encoder, payloader, capsfilter);
274
- for (const el of elements) this._pipeline.add(el);
275
-
276
- // Link source/tee → valve
277
- if (this._teeSrcPad) {
278
- const valveSinkPad = valve.get_static_pad('sink');
279
- this._teeSrcPad.link(valveSinkPad);
280
- } else if (sourceForChain) {
281
- sourceForChain.link(valve);
282
- }
283
- valve.link(convert);
284
- convert.link(resample);
285
- resample.link(encoder);
286
- encoder.link(payloader);
287
- payloader.link(capsfilter);
288
- lastElement = capsfilter;
289
- } else {
290
- // Video
291
- const convert = Gst.ElementFactory.make('videoconvert', null)!;
292
- const scale = Gst.ElementFactory.make('videoscale', null)!;
293
- const encoder = asVp8EncElement(Gst.ElementFactory.make('vp8enc', null)!);
294
- encoder.deadline = 1; // Realtime encoding
295
- encoder.keyframe_max_dist = 60;
296
- const payloader = asRtpPayloaderElement(Gst.ElementFactory.make('rtpvp8pay', null)!);
297
- payloader.pt = VP8_PAYLOAD_TYPE;
298
-
299
- const capsfilter = asCapsFilterElement(Gst.ElementFactory.make('capsfilter', null)!);
300
- capsfilter.caps = Gst.Caps.from_string(
301
- `application/x-rtp,media=video,encoding-name=VP8,clock-rate=90000,payload=${VP8_PAYLOAD_TYPE}`,
302
- );
303
-
304
- elements.push(convert, scale, encoder, payloader, capsfilter);
305
- for (const el of elements) this._pipeline.add(el);
306
-
307
- // Link source/tee → valve
308
- if (this._teeSrcPad) {
309
- const valveSinkPad = valve.get_static_pad('sink');
310
- this._teeSrcPad.link(valveSinkPad);
311
- } else if (sourceForChain) {
312
- sourceForChain.link(valve);
313
- }
314
- valve.link(convert);
315
- convert.link(scale);
316
- scale.link(encoder);
317
- encoder.link(payloader);
318
- payloader.link(capsfilter);
319
- lastElement = capsfilter;
320
- }
321
-
322
- // Link payloader output to webrtcbin sink pad.
323
- // Use the mline index if assigned (>= 0), otherwise request next available.
324
- const padName = this._mlineIndex >= 0 ? `sink_${this._mlineIndex}` : 'sink_%u';
325
- const sinkPad = this._webrtcbin.request_pad_simple
326
- ? this._webrtcbin.request_pad_simple(padName)
327
- : this._webrtcbin.get_request_pad(padName);
328
- if (sinkPad) {
329
- const srcPad = lastElement.get_static_pad('src');
330
- srcPad.link(sinkPad);
331
- }
332
-
333
- // Sync states — elements added to a PLAYING pipeline
334
- // If using a tee branch, don't include the shared source in our element list
335
- const ownedElements = this._teeSrcPad ? elements : [source, ...elements];
336
- for (const el of ownedElements) {
337
- el.sync_state_with_parent();
338
- }
339
-
340
- this._elements = ownedElements;
341
- this._linked = true;
342
-
343
- // Wire Track.enabled → valve.drop
344
- track._setEnableCallback((enabled: boolean) => {
345
- if (this._valve) this._valve.drop = !enabled;
346
- });
347
- }
348
-
349
- /** @internal — tear down the encoder chain on close/removeTrack */
350
- _teardownPipeline(): void {
351
- if (!this._linked) return;
352
- // Disconnect enable callback from the track
353
- if (this._track) {
354
- this._track._setEnableCallback(null);
355
- }
356
- // Release tee branch if using shared source
357
- if (this._teeSrcPad && this._track?._teeMultiplexer) {
358
- try {
359
- (this._track._teeMultiplexer as TeeMultiplexer).releaseSrcPad(this._teeSrcPad);
360
- } catch { /* ignore */ }
361
- this._teeSrcPad = null;
362
- }
363
- for (const el of [...this._elements].reverse()) {
364
- try {
365
- el.set_state(Gst.State.NULL);
366
- this._pipeline?.remove(el);
367
- } catch { /* ignore cleanup errors */ }
368
- }
369
- this._elements = [];
370
- this._valve = null;
371
- this._linked = false;
372
- }
373
-
374
- getParameters(): RTCRtpSendParameters {
375
- if (!this._lastParams) {
376
- this._lastParams = {
377
- transactionId: String(++_txCounter),
378
- encodings: [],
379
- codecs: [],
380
- headerExtensions: [],
381
- rtcp: {},
382
- };
383
- }
384
- return { ...this._lastParams, encodings: [...this._lastParams.encodings] };
385
- }
386
-
387
- async setParameters(params: RTCRtpSendParameters): Promise<void> {
388
- if (!this._lastParams) {
389
- throw new DOMException(
390
- 'getParameters must be called before setParameters',
391
- 'InvalidStateError',
392
- );
393
- }
394
- if (params.transactionId !== this._lastParams.transactionId) {
395
- throw new DOMException(
396
- 'transactionId mismatch',
397
- 'InvalidModificationError',
398
- );
399
- }
400
- this._lastParams = null;
401
- }
402
-
403
- async replaceTrack(track: MediaStreamTrack | null): Promise<void> {
404
- if (track === null) {
405
- this._teardownPipeline();
406
- this._track = null;
407
- return;
408
- }
409
- if (this._track !== null && track.kind !== this._track.kind) {
410
- throw new TypeError('Cannot replace track with different kind');
411
- }
412
- // Same narrowing as `_wirePipeline` — the GStreamer-backing fields on
413
- // `MediaStreamTrack` are runtime-attached and typed `any` on the class.
414
- const trackGst = track as MediaStreamTrack & {
415
- _gstSource: GstNs.Element | null;
416
- _gstPipeline: GstNs.Pipeline | null;
417
- };
418
- if (this._linked && trackGst._gstSource) {
419
- // Atomic source swap: old source → new source, keep rest of chain
420
- const oldSource = this._elements[0];
421
- const newSource = trackGst._gstSource;
422
-
423
- // Move new source from its pipeline
424
- const oldPipeline = trackGst._gstPipeline;
425
- if (oldPipeline && oldPipeline !== this._pipeline) {
426
- newSource.set_state(Gst.State.NULL);
427
- oldPipeline.remove(newSource);
428
- trackGst._gstPipeline = this._pipeline;
429
- }
430
-
431
- // Swap: unlink old, link new
432
- oldSource.set_state(Gst.State.NULL);
433
- if (this._valve) oldSource.unlink(this._valve);
434
- this._pipeline?.remove(oldSource);
435
-
436
- this._pipeline?.add(newSource);
437
- if (this._valve) newSource.link(this._valve);
438
- newSource.sync_state_with_parent();
439
-
440
- this._elements[0] = newSource;
441
- } else if (trackGst._gstSource) {
442
- this._wirePipeline(track);
443
- }
444
-
445
- // Disconnect old track's enable callback, wire new one
446
- if (this._track) this._track._setEnableCallback(null);
447
- this._track = track;
448
- if (this._linked) {
449
- track._setEnableCallback((enabled: boolean) => {
450
- if (this._valve) this._valve.drop = !enabled;
451
- });
452
- }
453
- }
454
-
455
- async getStats(): Promise<RTCStatsReport> {
456
- if (this._getStatsForTrack && this._track) {
457
- return this._getStatsForTrack(this._track);
458
- }
459
- // Fallback: return empty report if no PC or no track
460
- const { RTCStatsReport: Report } = await import('./rtc-stats-report.js');
461
- return new Report();
462
- }
463
-
464
- setStreams(..._streams: MediaStream[]): void {
465
- // Phase 3: no-op — webrtcbin manages msid in SDP automatically
466
- }
467
-
468
- static getCapabilities(kind: string): RTCRtpCapabilities | null {
469
- return getRtpCapabilities(kind);
470
- }
471
- }