@gjsify/webrtc 0.3.21 → 0.4.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.
Files changed (40) hide show
  1. package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
  2. package/lib/esm/get-user-media.js +1 -1
  3. package/lib/esm/gst-enum-maps.js +1 -1
  4. package/lib/esm/gst-init.js +1 -1
  5. package/lib/esm/gst-stats-parser.js +1 -1
  6. package/lib/esm/gst-utils.js +1 -1
  7. package/lib/esm/internal/gst-types.js +1 -0
  8. package/lib/esm/media-device-info.js +1 -1
  9. package/lib/esm/media-devices.js +1 -1
  10. package/lib/esm/media-stream-track.js +1 -1
  11. package/lib/esm/media-stream.js +1 -1
  12. package/lib/esm/rtc-certificate.js +1 -1
  13. package/lib/esm/rtc-data-channel.js +1 -1
  14. package/lib/esm/rtc-dtls-transport.js +1 -1
  15. package/lib/esm/rtc-dtmf-sender.js +1 -1
  16. package/lib/esm/rtc-error.js +1 -1
  17. package/lib/esm/rtc-events.js +1 -1
  18. package/lib/esm/rtc-ice-candidate.js +1 -1
  19. package/lib/esm/rtc-ice-transport.js +1 -1
  20. package/lib/esm/rtc-peer-connection.js +1 -1
  21. package/lib/esm/rtc-rtp-receiver.js +1 -1
  22. package/lib/esm/rtc-rtp-sender.js +1 -1
  23. package/lib/esm/rtc-rtp-transceiver.js +1 -1
  24. package/lib/esm/rtc-sctp-transport.js +1 -1
  25. package/lib/esm/rtc-session-description.js +1 -1
  26. package/lib/esm/rtc-stats-report.js +1 -1
  27. package/lib/esm/rtc-track-event.js +1 -1
  28. package/lib/esm/rtp-capabilities.js +1 -1
  29. package/lib/esm/tee-multiplexer.js +1 -1
  30. package/lib/esm/wpt-helpers.js +1 -1
  31. package/lib/types/gst-enum-maps.d.ts +2 -1
  32. package/lib/types/internal/gst-types.d.ts +83 -0
  33. package/lib/types/rtc-rtp-sender.d.ts +3 -2
  34. package/package.json +13 -13
  35. package/src/gst-enum-maps.ts +1 -1
  36. package/src/internal/gst-types.ts +122 -0
  37. package/src/rtc-peer-connection.ts +44 -28
  38. package/src/rtc-rtp-sender.ts +76 -49
  39. package/src/rtc-rtp-transceiver.ts +8 -4
  40. package/tsconfig.tsbuildinfo +1 -1
@@ -0,0 +1,122 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Internal type helpers for @gjsify/webrtc — narrowing the broad
3
+ // Gst.Element / Gst.Pad surface to the concrete element shapes our
4
+ // implementation reads and writes at runtime.
5
+ //
6
+ // Background: `@girs/gst-1.0` declares every GStreamer element as the
7
+ // base `Gst.Element` class — element-specific GObject properties (e.g.
8
+ // `webrtcbin`'s `stun_server`, `signaling_state`, or `vp8enc`'s
9
+ // `keyframe_max_dist`) are not exposed in the GIR-generated typings
10
+ // because they are registered at element-class init time, not on the
11
+ // base class. Rather than reach for `(el as any).foo` at every call
12
+ // site, we declare thin interfaces here and narrow once through helper
13
+ // casts.
14
+ //
15
+ // These types are PURE compile-time constructs — no runtime behavior is
16
+ // added. Following AGENTS.md Rule 2c, this module lives under
17
+ // `src/internal/` and is NOT in `package.json#exports`; it is a private
18
+ // implementation helper, not part of the public API surface.
19
+ //
20
+ // References:
21
+ // - GStreamer webrtcbin element properties:
22
+ // https://gstreamer.freedesktop.org/documentation/webrtc/index.html
23
+ // - vp8enc / opusenc / capsfilter / valve / payloader properties:
24
+ // https://gstreamer.freedesktop.org/documentation/
25
+
26
+ import type Gst from 'gi://Gst?version=1.0';
27
+ import type GstWebRTC from 'gi://GstWebRTC?version=1.0';
28
+
29
+ /**
30
+ * The `webrtcbin` GStreamer element — declares every GObject property
31
+ * our implementation accesses. The base `Gst.Element` class is preserved
32
+ * so all the inherited methods (`emit`, `get_parent`, `set_state`,
33
+ * `request_pad_simple`, `sync_state_with_parent`, …) keep their proper
34
+ * typings.
35
+ *
36
+ * All properties below correspond to runtime GObject properties on
37
+ * `webrtcbin` (verify with `gst-inspect-1.0 webrtcbin`).
38
+ */
39
+ export interface WebRtcBin extends Gst.Element {
40
+ stun_server: string | null;
41
+ turn_server: string | null;
42
+ ice_transport_policy: GstWebRTC.WebRTCICETransportPolicy;
43
+ bundle_policy: GstWebRTC.WebRTCBundlePolicy;
44
+ signaling_state: GstWebRTC.WebRTCSignalingState;
45
+ connection_state: GstWebRTC.WebRTCPeerConnectionState;
46
+ ice_connection_state: GstWebRTC.WebRTCICEConnectionState;
47
+ ice_gathering_state: GstWebRTC.WebRTCICEGatheringState;
48
+ local_description: GstWebRTC.WebRTCSessionDescription | null;
49
+ remote_description: GstWebRTC.WebRTCSessionDescription | null;
50
+ current_local_description: GstWebRTC.WebRTCSessionDescription | null;
51
+ current_remote_description: GstWebRTC.WebRTCSessionDescription | null;
52
+ pending_local_description: GstWebRTC.WebRTCSessionDescription | null;
53
+ pending_remote_description: GstWebRTC.WebRTCSessionDescription | null;
54
+ }
55
+
56
+ /**
57
+ * Narrow a `Gst.Element` returned by `Gst.ElementFactory.make('webrtcbin', …)`
58
+ * to the augmented `WebRtcBin` shape. Pure type-level cast — no runtime
59
+ * validation is performed because every webrtcbin instance has these
60
+ * properties (they are class-installed by GstWebRTCBin).
61
+ */
62
+ export const asWebRtcBin = (el: Gst.Element): WebRtcBin => el as WebRtcBin;
63
+
64
+ /**
65
+ * GStreamer pad augmented with the `transceiver` field that webrtcbin's
66
+ * SRC pads carry. The base `Gst.Pad` typing has no concept of this — it
67
+ * is set at pad-creation time inside webrtcbin and points back to the
68
+ * `GstWebRTCRTPTransceiver` the pad serves.
69
+ */
70
+ export interface WebRtcSrcPad extends Gst.Pad {
71
+ transceiver: GstWebRTC.WebRTCRTPTransceiver | null;
72
+ }
73
+
74
+ /** Narrow a webrtcbin SRC `Gst.Pad` to the augmented shape. */
75
+ export const asWebRtcSrcPad = (pad: Gst.Pad): WebRtcSrcPad => pad as WebRtcSrcPad;
76
+
77
+ /**
78
+ * GStreamer element with a settable `drop` GObject property — the
79
+ * `valve` element used to gate media flow when a track is disabled.
80
+ */
81
+ export interface ValveElement extends Gst.Element {
82
+ drop: boolean;
83
+ }
84
+
85
+ /** Narrow a `valve` element to the augmented shape. */
86
+ export const asValveElement = (el: Gst.Element): ValveElement => el as ValveElement;
87
+
88
+ /**
89
+ * GStreamer element with a settable `pt` GObject property — RTP payloader
90
+ * elements (`rtpopuspay`, `rtpvp8pay`, …) accept the payload type.
91
+ */
92
+ export interface RtpPayloaderElement extends Gst.Element {
93
+ pt: number;
94
+ }
95
+
96
+ /** Narrow an RTP payloader element to the augmented shape. */
97
+ export const asRtpPayloaderElement = (el: Gst.Element): RtpPayloaderElement =>
98
+ el as RtpPayloaderElement;
99
+
100
+ /**
101
+ * GStreamer `capsfilter` element — exposes a settable `caps` property
102
+ * that pins the negotiated caps of a pipeline branch.
103
+ */
104
+ export interface CapsFilterElement extends Gst.Element {
105
+ caps: Gst.Caps;
106
+ }
107
+
108
+ /** Narrow a `capsfilter` element to the augmented shape. */
109
+ export const asCapsFilterElement = (el: Gst.Element): CapsFilterElement =>
110
+ el as CapsFilterElement;
111
+
112
+ /**
113
+ * GStreamer `vp8enc` encoder — the small set of properties we tune for
114
+ * realtime WebRTC video encoding.
115
+ */
116
+ export interface Vp8EncElement extends Gst.Element {
117
+ deadline: number;
118
+ keyframe_max_dist: number;
119
+ }
120
+
121
+ /** Narrow a `vp8enc` element to the augmented shape. */
122
+ export const asVp8EncElement = (el: Gst.Element): Vp8EncElement => el as Vp8EncElement;
@@ -22,6 +22,7 @@ import {
22
22
  gstToIceGatheringState,
23
23
  w3cDirectionToGst,
24
24
  } from './gst-enum-maps.js';
25
+ import { asWebRtcBin, asWebRtcSrcPad } from './internal/gst-types.js';
25
26
  import { DOMException } from '@gjsify/dom-exception';
26
27
  import { RTCSessionDescription, type RTCSessionDescriptionInit } from './rtc-session-description.js';
27
28
  import { RTCIceCandidate, type RTCIceCandidateInit } from './rtc-ice-candidate.js';
@@ -179,7 +180,7 @@ export class RTCPeerConnection extends EventTarget {
179
180
  // Connect via @gjsify/webrtc-native's WebrtcbinBridge — webrtcbin fires
180
181
  // its signals from the streaming thread, GJS would block direct JS
181
182
  // callbacks. The bridge hops to the main context on the C side.
182
- this._bridge = new (WebrtcbinBridge as any)({ bin: this._webrtcbin });
183
+ this._bridge = new WebrtcbinBridge({ bin: this._webrtcbin });
183
184
  this._bridge.connect('negotiation-needed', () => this._handleNegotiationNeeded());
184
185
  this._bridge.connect('icecandidate', (_b, mlineIndex, candidate) =>
185
186
  this._handleIceCandidate(mlineIndex, candidate));
@@ -225,7 +226,7 @@ export class RTCPeerConnection extends EventTarget {
225
226
 
226
227
  if (proto === 'stun:' || proto === 'stuns:') {
227
228
  if (stunSet) continue; // webrtcbin supports only one STUN server
228
- (this._webrtcbin as any).stun_server = `${proto}//${hostPort}`;
229
+ asWebRtcBin(this._webrtcbin).stun_server = `${proto}//${hostPort}`;
229
230
  stunSet = true;
230
231
  } else if (proto === 'turn:' || proto === 'turns:') {
231
232
  if (typeof server.username !== 'string' || typeof server.credential !== 'string') {
@@ -237,7 +238,7 @@ export class RTCPeerConnection extends EventTarget {
237
238
  try {
238
239
  this._webrtcbin.emit('add-turn-server', turnUrl);
239
240
  } catch {
240
- (this._webrtcbin as any).turn_server = turnUrl;
241
+ asWebRtcBin(this._webrtcbin).turn_server = turnUrl;
241
242
  }
242
243
  } else {
243
244
  throw new TypeError(`Unsupported ICE server protocol "${proto}"`);
@@ -251,49 +252,53 @@ export class RTCPeerConnection extends EventTarget {
251
252
  const gstPolicy = policy === 'relay'
252
253
  ? GstWebRTC.WebRTCICETransportPolicy.RELAY
253
254
  : GstWebRTC.WebRTCICETransportPolicy.ALL;
254
- try { (this._webrtcbin as any).ice_transport_policy = gstPolicy; } catch { /* ignore */ }
255
+ try { asWebRtcBin(this._webrtcbin).ice_transport_policy = gstPolicy; } catch { /* ignore */ }
255
256
  }
256
257
 
257
258
  private _applyBundlePolicy(policy?: RTCBundlePolicy): void {
258
259
  if (!policy) return;
259
- let gstPolicy: number;
260
+ let gstPolicy: GstWebRTC.WebRTCBundlePolicy;
260
261
  switch (policy) {
261
262
  case 'balanced': gstPolicy = GstWebRTC.WebRTCBundlePolicy.BALANCED; break;
262
263
  case 'max-compat': gstPolicy = GstWebRTC.WebRTCBundlePolicy.MAX_COMPAT; break;
263
264
  case 'max-bundle': gstPolicy = GstWebRTC.WebRTCBundlePolicy.MAX_BUNDLE; break;
264
265
  default: return;
265
266
  }
266
- try { (this._webrtcbin as any).bundle_policy = gstPolicy; } catch { /* ignore */ }
267
+ try { asWebRtcBin(this._webrtcbin).bundle_policy = gstPolicy; } catch { /* ignore */ }
267
268
  }
268
269
 
269
270
  // ---- Properties --------------------------------------------------------
270
271
 
271
272
  get signalingState(): RTCSignalingState {
272
273
  if (this._closed) return 'closed';
273
- try { return gstToSignalingState((this._webrtcbin as any).signaling_state); }
274
+ try { return gstToSignalingState(asWebRtcBin(this._webrtcbin).signaling_state); }
274
275
  catch { return 'stable'; }
275
276
  }
276
277
 
277
278
  get connectionState(): RTCPeerConnectionState {
278
279
  if (this._closed) return 'closed';
279
- try { return gstToConnectionState((this._webrtcbin as any).connection_state); }
280
+ try { return gstToConnectionState(asWebRtcBin(this._webrtcbin).connection_state); }
280
281
  catch { return 'new'; }
281
282
  }
282
283
 
283
284
  get iceConnectionState(): RTCIceConnectionState {
284
285
  if (this._closed) return 'closed';
285
- try { return gstToIceConnectionState((this._webrtcbin as any).ice_connection_state); }
286
+ try { return gstToIceConnectionState(asWebRtcBin(this._webrtcbin).ice_connection_state); }
286
287
  catch { return 'new'; }
287
288
  }
288
289
 
289
290
  get iceGatheringState(): RTCIceGatheringState {
290
- try { return gstToIceGatheringState((this._webrtcbin as any).ice_gathering_state); }
291
+ try { return gstToIceGatheringState(asWebRtcBin(this._webrtcbin).ice_gathering_state); }
291
292
  catch { return 'new'; }
292
293
  }
293
294
 
294
- private _descProp(prop: string): RTCSessionDescription | null {
295
+ private _descProp(
296
+ prop: 'local_description' | 'remote_description'
297
+ | 'current_local_description' | 'current_remote_description'
298
+ | 'pending_local_description' | 'pending_remote_description',
299
+ ): RTCSessionDescription | null {
295
300
  try {
296
- const desc = (this._webrtcbin as any)[prop] as GstWebRTC.WebRTCSessionDescription | null;
301
+ const desc = asWebRtcBin(this._webrtcbin)[prop];
297
302
  if (!desc) return null;
298
303
  return RTCSessionDescription.fromGstDesc(desc);
299
304
  } catch { return null; }
@@ -453,7 +458,11 @@ export class RTCPeerConnection extends EventTarget {
453
458
 
454
459
  let native: GstWebRTC.WebRTCDataChannel | null = null;
455
460
  try {
456
- native = this._webrtcbin.emit('create-data-channel', label, gstOpts) as any;
461
+ // webrtcbin's `create-data-channel` is an action signal that returns
462
+ // a `GstWebRTCDataChannel`. The GIR-generated `emit()` overloads
463
+ // declare a `void` return for action signals, but at runtime the
464
+ // value flows back. Cast through `unknown` to acknowledge the gap.
465
+ native = this._webrtcbin.emit('create-data-channel', label, gstOpts) as unknown as GstWebRTC.WebRTCDataChannel | null;
457
466
  } catch (err: any) {
458
467
  throw new Error(`create-data-channel failed: ${err?.message ?? err}`);
459
468
  }
@@ -572,10 +581,10 @@ export class RTCPeerConnection extends EventTarget {
572
581
  `Failed to execute 'addTransceiver' on 'RTCPeerConnection': The provided value '${direction}' is not a valid enum value of type RTCRtpTransceiverDirection.`,
573
582
  );
574
583
  }
575
- const hasGstSource = trackOrKind instanceof MediaStreamTrack && (trackOrKind as any)._gstSource;
584
+ const hasGstSource = trackOrKind instanceof MediaStreamTrack && trackOrKind._gstSource;
576
585
  const wantsSend = direction === 'sendrecv' || direction === 'sendonly';
577
586
 
578
- let gstTrans: any;
587
+ let gstTrans: GstWebRTC.WebRTCRTPTransceiver;
579
588
  let jsTrans: RTCRtpTransceiver;
580
589
 
581
590
  if (hasGstSource && wantsSend) {
@@ -594,10 +603,11 @@ export class RTCPeerConnection extends EventTarget {
594
603
  sender._wirePipeline(track);
595
604
 
596
605
  // Find the GstTransceiver that request_pad_simple created
597
- gstTrans = this._findNewGstTransceiver();
598
- if (!gstTrans) {
606
+ const found = this._findNewGstTransceiver();
607
+ if (!found) {
599
608
  throw new Error('webrtcbin did not create a transceiver for the send pad');
600
609
  }
610
+ gstTrans = found;
601
611
 
602
612
  // Create wrapper with the pre-wired sender
603
613
  const gstReceiver = gstTrans.receiver ?? null;
@@ -618,7 +628,7 @@ export class RTCPeerConnection extends EventTarget {
618
628
  this._receivers.push(receiver);
619
629
 
620
630
  // Apply direction
621
- (gstTrans as any).direction = w3cDirectionToGst(direction);
631
+ gstTrans.direction = w3cDirectionToGst(direction);
622
632
  } else {
623
633
  // Path B: No GStreamer source, or receive-only/inactive.
624
634
  // Use emit('add-transceiver') which creates a transceiver without pads.
@@ -629,17 +639,20 @@ export class RTCPeerConnection extends EventTarget {
629
639
  ? w3cDirectionToGst('sendrecv')
630
640
  : w3cDirectionToGst(direction);
631
641
 
632
- gstTrans = this._webrtcbin.emit('add-transceiver', createDirection, caps) as any;
633
- if (!gstTrans) {
642
+ // `add-transceiver` is an action signal returning the new
643
+ // GstWebRTCRTPTransceiver — see comment on `create-data-channel` above.
644
+ const result = this._webrtcbin.emit('add-transceiver', createDirection, caps) as unknown as GstWebRTC.WebRTCRTPTransceiver | null;
645
+ if (!result) {
634
646
  throw new Error('webrtcbin did not create a transceiver');
635
647
  }
648
+ gstTrans = result;
636
649
 
637
650
  jsTrans = this._transceivers.get(gstTrans)!;
638
651
  if (!jsTrans) {
639
652
  jsTrans = this._createTransceiverWrapper(gstTrans);
640
653
  }
641
654
 
642
- (gstTrans as any).direction = w3cDirectionToGst(direction);
655
+ gstTrans.direction = w3cDirectionToGst(direction);
643
656
 
644
657
  if (trackOrKind instanceof MediaStreamTrack) {
645
658
  jsTrans.sender._setTrack(trackOrKind);
@@ -797,9 +810,11 @@ export class RTCPeerConnection extends EventTarget {
797
810
  // ---- Transceiver helper -------------------------------------------------
798
811
 
799
812
  /** Find a GstWebRTCRTPTransceiver not yet in our map (created by request_pad_simple). */
800
- private _findNewGstTransceiver(): any {
813
+ private _findNewGstTransceiver(): GstWebRTC.WebRTCRTPTransceiver | null {
801
814
  for (let i = 0; ; i++) {
802
- const gt = this._webrtcbin.emit('get-transceiver', i) as any;
815
+ // `get-transceiver` is an action signal — return value flows back at
816
+ // runtime even though the GIR `emit()` overload is typed `void`.
817
+ const gt = this._webrtcbin.emit('get-transceiver', i) as unknown as GstWebRTC.WebRTCRTPTransceiver | null;
803
818
  if (!gt) return null;
804
819
  if (!this._transceivers.has(gt)) return gt;
805
820
  }
@@ -821,7 +836,7 @@ export class RTCPeerConnection extends EventTarget {
821
836
  this._sctpTransport = new RTCSctpTransport(dtls);
822
837
  }
823
838
 
824
- private _createTransceiverWrapper(gstTrans: any): RTCRtpTransceiver {
839
+ private _createTransceiverWrapper(gstTrans: GstWebRTC.WebRTCRTPTransceiver): RTCRtpTransceiver {
825
840
  let kind: 'audio' | 'video' = 'audio';
826
841
  try {
827
842
  const gstKind = gstTrans.kind;
@@ -848,7 +863,7 @@ export class RTCPeerConnection extends EventTarget {
848
863
 
849
864
  // Pass mline index to sender for sink pad naming
850
865
  try {
851
- const mline = (gstTrans as any).mlineindex;
866
+ const mline = gstTrans.mlineindex;
852
867
  if (typeof mline === 'number' && mline >= 0) {
853
868
  sender._setMlineIndex(mline);
854
869
  }
@@ -892,7 +907,7 @@ export class RTCPeerConnection extends EventTarget {
892
907
  // Only process SRC pads (incoming media from remote peer)
893
908
  if (pad.direction !== Gst.PadDirection.SRC) return;
894
909
 
895
- const gstTrans = (pad as any).transceiver;
910
+ const gstTrans = asWebRtcSrcPad(pad).transceiver;
896
911
  if (!gstTrans) return;
897
912
 
898
913
  let jsTrans = this._transceivers.get(gstTrans);
@@ -974,14 +989,15 @@ export class RTCPeerConnection extends EventTarget {
974
989
  private _syncIceState(): void {
975
990
  if (!this._iceTransport) return;
976
991
  const iceState = this.iceConnectionState;
977
- this._iceTransport._setState(iceState as any);
992
+ // RTCIceConnectionState ≡ RTCIceTransportState (same string union).
993
+ this._iceTransport._setState(iceState);
978
994
  }
979
995
 
980
996
  /** Map PC ICE gathering state → ICE transport gathering state. */
981
997
  private _syncIceGatheringState(): void {
982
998
  if (!this._iceTransport) return;
983
999
  const gatheringState = this.iceGatheringState;
984
- this._iceTransport._setGatheringState(gatheringState as any);
1000
+ this._iceTransport._setGatheringState(gatheringState);
985
1001
  }
986
1002
 
987
1003
  // ---- on<event> attribute handlers --------------------------------------
@@ -8,12 +8,20 @@
8
8
  // Reference: refs/node-gst-webrtc/src/webrtc/RTCRtpSender.ts (ISC)
9
9
  // Reference: W3C WebRTC spec § 5.2
10
10
 
11
+ import type GstNs from 'gi://Gst?version=1.0';
11
12
  import type GstWebRTC from 'gi://GstWebRTC?version=1.0';
12
13
 
13
14
  import { Gst } from './gst-init.js';
14
15
  import { getRtpCapabilities } from './rtp-capabilities.js';
15
16
  import { RTCDTMFSender } from './rtc-dtmf-sender.js';
16
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';
17
25
  import type { RTCStatsReport } from './rtc-stats-report.js';
18
26
  import type { RTCDtlsTransport } from './rtc-dtls-transport.js';
19
27
  import type { MediaStreamTrack } from './media-stream-track.js';
@@ -84,14 +92,14 @@ export class RTCRtpSender {
84
92
  private _lastParams: RTCRtpSendParameters | null = null;
85
93
 
86
94
  /** @internal GStreamer pipeline references (set by RTCPeerConnection) */
87
- private _pipeline: any = null;
88
- private _webrtcbin: any = null;
95
+ private _pipeline: GstNs.Pipeline | null = null;
96
+ private _webrtcbin: GstNs.Element | null = null;
89
97
  private _mlineIndex: number = -1;
90
- private _elements: any[] = [];
91
- private _valve: any = null;
98
+ private _elements: GstNs.Element[] = [];
99
+ private _valve: ValveElement | null = null;
92
100
  _linked = false;
93
101
  /** @internal — tee src pad if this sender uses a shared source */
94
- private _teeSrcPad: any = null;
102
+ private _teeSrcPad: GstNs.Pad | null = null;
95
103
  /** @internal — stats callback set by RTCPeerConnection */
96
104
  _getStatsForTrack: ((track: MediaStreamTrack) => Promise<RTCStatsReport>) | null = null;
97
105
  /** @internal — set by RTCPeerConnection */
@@ -103,9 +111,13 @@ export class RTCRtpSender {
103
111
  /** @internal — back-reference for DTMF stopped/direction checks */
104
112
  _transceiver: { stopped: boolean; currentDirection: string | null } | null = null;
105
113
  /** @internal — callback to notify RTCPeerConnection when pipeline changes (cross-pipeline fix) */
106
- _onPipelineChanged: ((newPipeline: any) => void) | null = null;
114
+ _onPipelineChanged: ((newPipeline: GstNs.Pipeline) => void) | null = null;
107
115
 
108
- constructor(gstSender: GstWebRTC.WebRTCRTPSender | null, pipeline?: any, webrtcbin?: any) {
116
+ constructor(
117
+ gstSender: GstWebRTC.WebRTCRTPSender | null,
118
+ pipeline?: GstNs.Pipeline,
119
+ webrtcbin?: GstNs.Element,
120
+ ) {
109
121
  this._gstSender = gstSender;
110
122
  this._pipeline = pipeline ?? null;
111
123
  this._webrtcbin = webrtcbin ?? null;
@@ -143,19 +155,28 @@ export class RTCRtpSender {
143
155
  /** @internal — build the outgoing encoder chain and link to webrtcbin */
144
156
  _wirePipeline(track: MediaStreamTrack): void {
145
157
  if (this._linked || !this._pipeline || !this._webrtcbin) return;
146
- const source = (track as any)._gstSource;
158
+ const source = track._gstSource as GstNs.Element | null;
147
159
  if (!source) return; // No GStreamer backing — nothing to wire
148
160
 
149
- const trackAny = track as any;
150
- let sourceForChain: any; // What to link to the valve (source directly or tee branch)
151
-
152
- if (trackAny._gstTee && trackAny._gstPipeline && trackAny._gstPipeline !== this._pipeline) {
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) {
153
174
  // Source has a tee from VideoBridge (preview) in a different pipeline.
154
175
  // Instead of moving the source, we add our encoder chain elements to
155
176
  // the SOURCE's pipeline and request a new branch from its tee.
156
177
  // The webrtcbin must also move to the source pipeline.
157
- const sourcePipeline = trackAny._gstPipeline;
158
- const tee = trackAny._gstTee;
178
+ const sourcePipeline = trackGst._gstPipeline;
179
+ const tee = trackGst._gstTee;
159
180
 
160
181
  // Move webrtcbin to the source pipeline so everything is in one pipeline
161
182
  if (this._webrtcbin.get_parent() === this._pipeline) {
@@ -176,17 +197,17 @@ export class RTCRtpSender {
176
197
  : tee.get_request_pad('src_%u');
177
198
  this._teeSrcPad = teeSrcPad;
178
199
  sourceForChain = null; // We'll link via pad below
179
- } else if (trackAny._teeMultiplexer) {
200
+ } else if (trackGst._teeMultiplexer) {
180
201
  // Track already has a TeeMultiplexer (shared with another PC) — request a new branch
181
- const tee = trackAny._teeMultiplexer as TeeMultiplexer;
202
+ const tee = trackGst._teeMultiplexer as TeeMultiplexer;
182
203
  const teeSrcPad = tee.requestSrcPad();
183
204
  this._teeSrcPad = teeSrcPad;
184
205
  sourceForChain = null; // We'll link via pad below
185
- } else if (trackAny._gstPipeline && trackAny._gstPipeline !== this._pipeline) {
206
+ } else if (trackGst._gstPipeline && trackGst._gstPipeline !== this._pipeline) {
186
207
  // Source is in another pipeline (no tee from VideoBridge) — this is the
187
208
  // second PC using this track. Insert a tee between source and the
188
209
  // existing consumer. Move source to this pipeline and create a tee.
189
- const oldPipeline = trackAny._gstPipeline;
210
+ const oldPipeline = trackGst._gstPipeline;
190
211
 
191
212
  // First, unlink source from its current peer (the first sender's valve)
192
213
  const sourceSrcPad = source.get_static_pad('src');
@@ -196,11 +217,11 @@ export class RTCRtpSender {
196
217
  source.set_state(Gst.State.NULL);
197
218
  oldPipeline.remove(source);
198
219
  this._pipeline.add(source);
199
- trackAny._gstPipeline = this._pipeline;
220
+ trackGst._gstPipeline = this._pipeline;
200
221
 
201
222
  // Create tee in this pipeline
202
223
  const tee = new TeeMultiplexer(this._pipeline, source);
203
- trackAny._teeMultiplexer = tee;
224
+ trackGst._teeMultiplexer = tee;
204
225
 
205
226
  // Reconnect the old consumer (first sender) via a tee branch
206
227
  if (oldPeer) {
@@ -214,11 +235,11 @@ export class RTCRtpSender {
214
235
  sourceForChain = null;
215
236
  } else {
216
237
  // First PC to use this track — move source directly
217
- const oldPipeline = trackAny._gstPipeline;
238
+ const oldPipeline = trackGst._gstPipeline;
218
239
  if (oldPipeline && oldPipeline !== this._pipeline) {
219
240
  source.set_state(Gst.State.NULL);
220
241
  oldPipeline.remove(source);
221
- trackAny._gstPipeline = this._pipeline;
242
+ trackGst._gstPipeline = this._pipeline;
222
243
  }
223
244
  if (source.get_parent() !== this._pipeline) {
224
245
  this._pipeline.add(source);
@@ -227,25 +248,25 @@ export class RTCRtpSender {
227
248
  }
228
249
 
229
250
  // Valve element for Track.enabled control
230
- const valve = Gst.ElementFactory.make('valve', null)!;
231
- (valve as any).drop = !track.enabled;
251
+ const valve = asValveElement(Gst.ElementFactory.make('valve', null)!);
252
+ valve.drop = !track.enabled;
232
253
  this._valve = valve;
233
254
  this._pipeline.add(valve);
234
255
 
235
- const elements: any[] = [valve];
236
- let lastElement: any;
256
+ const elements: GstNs.Element[] = [valve];
257
+ let lastElement: GstNs.Element;
237
258
 
238
259
  if (track.kind === 'audio') {
239
260
  const convert = Gst.ElementFactory.make('audioconvert', null)!;
240
261
  const resample = Gst.ElementFactory.make('audioresample', null)!;
241
262
  const encoder = Gst.ElementFactory.make('opusenc', null)!;
242
- const payloader = Gst.ElementFactory.make('rtpopuspay', null)!;
243
- (payloader as any).pt = OPUS_PAYLOAD_TYPE;
263
+ const payloader = asRtpPayloaderElement(Gst.ElementFactory.make('rtpopuspay', null)!);
264
+ payloader.pt = OPUS_PAYLOAD_TYPE;
244
265
 
245
266
  // capsfilter tells webrtcbin the RTP caps immediately so createOffer
246
267
  // can generate the m=audio line without waiting for data to flow.
247
- const capsfilter = Gst.ElementFactory.make('capsfilter', null)!;
248
- (capsfilter as any).caps = Gst.Caps.from_string(
268
+ const capsfilter = asCapsFilterElement(Gst.ElementFactory.make('capsfilter', null)!);
269
+ capsfilter.caps = Gst.Caps.from_string(
249
270
  `application/x-rtp,media=audio,encoding-name=OPUS,clock-rate=48000,payload=${OPUS_PAYLOAD_TYPE}`,
250
271
  );
251
272
 
@@ -269,14 +290,14 @@ export class RTCRtpSender {
269
290
  // Video
270
291
  const convert = Gst.ElementFactory.make('videoconvert', null)!;
271
292
  const scale = Gst.ElementFactory.make('videoscale', null)!;
272
- const encoder = Gst.ElementFactory.make('vp8enc', null)!;
273
- (encoder as any).deadline = 1; // Realtime encoding
274
- (encoder as any).keyframe_max_dist = 60;
275
- const payloader = Gst.ElementFactory.make('rtpvp8pay', null)!;
276
- (payloader as any).pt = VP8_PAYLOAD_TYPE;
277
-
278
- const capsfilter = Gst.ElementFactory.make('capsfilter', null)!;
279
- (capsfilter as any).caps = Gst.Caps.from_string(
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(
280
301
  `application/x-rtp,media=video,encoding-name=VP8,clock-rate=90000,payload=${VP8_PAYLOAD_TYPE}`,
281
302
  );
282
303
 
@@ -321,7 +342,7 @@ export class RTCRtpSender {
321
342
 
322
343
  // Wire Track.enabled → valve.drop
323
344
  track._setEnableCallback((enabled: boolean) => {
324
- if (this._valve) (this._valve as any).drop = !enabled;
345
+ if (this._valve) this._valve.drop = !enabled;
325
346
  });
326
347
  }
327
348
 
@@ -388,30 +409,36 @@ export class RTCRtpSender {
388
409
  if (this._track !== null && track.kind !== this._track.kind) {
389
410
  throw new TypeError('Cannot replace track with different kind');
390
411
  }
391
- if (this._linked && (track as any)._gstSource) {
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) {
392
419
  // Atomic source swap: old source → new source, keep rest of chain
393
420
  const oldSource = this._elements[0];
394
- const newSource = (track as any)._gstSource;
421
+ const newSource = trackGst._gstSource;
395
422
 
396
423
  // Move new source from its pipeline
397
- const oldPipeline = (track as any)._gstPipeline;
424
+ const oldPipeline = trackGst._gstPipeline;
398
425
  if (oldPipeline && oldPipeline !== this._pipeline) {
399
426
  newSource.set_state(Gst.State.NULL);
400
427
  oldPipeline.remove(newSource);
401
- (track as any)._gstPipeline = this._pipeline;
428
+ trackGst._gstPipeline = this._pipeline;
402
429
  }
403
430
 
404
431
  // Swap: unlink old, link new
405
432
  oldSource.set_state(Gst.State.NULL);
406
- oldSource.unlink(this._valve);
407
- this._pipeline.remove(oldSource);
433
+ if (this._valve) oldSource.unlink(this._valve);
434
+ this._pipeline?.remove(oldSource);
408
435
 
409
- this._pipeline.add(newSource);
410
- newSource.link(this._valve);
436
+ this._pipeline?.add(newSource);
437
+ if (this._valve) newSource.link(this._valve);
411
438
  newSource.sync_state_with_parent();
412
439
 
413
440
  this._elements[0] = newSource;
414
- } else if ((track as any)._gstSource) {
441
+ } else if (trackGst._gstSource) {
415
442
  this._wirePipeline(track);
416
443
  }
417
444
 
@@ -420,7 +447,7 @@ export class RTCRtpSender {
420
447
  this._track = track;
421
448
  if (this._linked) {
422
449
  track._setEnableCallback((enabled: boolean) => {
423
- if (this._valve) (this._valve as any).drop = !enabled;
450
+ if (this._valve) this._valve.drop = !enabled;
424
451
  });
425
452
  }
426
453
  }
@@ -31,13 +31,13 @@ export class RTCRtpTransceiver {
31
31
 
32
32
  get mid(): string | null {
33
33
  if (this._stopped) return null;
34
- const m = (this._gstTrans as any).mid;
34
+ const m = this._gstTrans.mid;
35
35
  return (m === '' || m == null) ? null : String(m);
36
36
  }
37
37
 
38
38
  get direction(): RTCRtpTransceiverDirection {
39
39
  if (this._stopped) return 'stopped';
40
- return gstDirectionToW3C((this._gstTrans as any).direction);
40
+ return gstDirectionToW3C(this._gstTrans.direction);
41
41
  }
42
42
 
43
43
  set direction(d: RTCRtpTransceiverDirection) {
@@ -54,12 +54,16 @@ export class RTCRtpTransceiver {
54
54
  if (!valid.includes(d)) {
55
55
  throw new TypeError(`The provided value '${d}' is not a valid enum value of type RTCRtpTransceiverDirection.`);
56
56
  }
57
- (this._gstTrans as any).direction = w3cDirectionToGst(d);
57
+ this._gstTrans.direction = w3cDirectionToGst(d);
58
58
  }
59
59
 
60
60
  get currentDirection(): RTCRtpTransceiverDirection | null {
61
61
  if (this._stopped) return null;
62
- const cd = (this._gstTrans as any).current_direction ?? (this._gstTrans as any).currentDirection;
62
+ // GIR exposes both snake_case `current_direction` and camelCase
63
+ // `currentDirection` getters — they refer to the same GObject
64
+ // property. Read snake_case first; fall back to camelCase for
65
+ // older GstWebRTC bindings that omitted the snake_case alias.
66
+ const cd = this._gstTrans.current_direction ?? this._gstTrans.currentDirection;
63
67
  if (cd == null) return null;
64
68
  const w3c = gstDirectionToW3C(cd);
65
69
  return w3c === 'inactive' ? null : w3c;