@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.
- package/lib/esm/_virtual/_rolldown/runtime.js +1 -0
- package/lib/esm/get-user-media.js +1 -1
- package/lib/esm/gst-enum-maps.js +1 -1
- package/lib/esm/gst-init.js +1 -1
- package/lib/esm/gst-stats-parser.js +1 -1
- package/lib/esm/gst-utils.js +1 -1
- package/lib/esm/internal/gst-types.js +1 -0
- package/lib/esm/media-device-info.js +1 -1
- package/lib/esm/media-devices.js +1 -1
- package/lib/esm/media-stream-track.js +1 -1
- package/lib/esm/media-stream.js +1 -1
- package/lib/esm/rtc-certificate.js +1 -1
- package/lib/esm/rtc-data-channel.js +1 -1
- package/lib/esm/rtc-dtls-transport.js +1 -1
- package/lib/esm/rtc-dtmf-sender.js +1 -1
- package/lib/esm/rtc-error.js +1 -1
- package/lib/esm/rtc-events.js +1 -1
- package/lib/esm/rtc-ice-candidate.js +1 -1
- package/lib/esm/rtc-ice-transport.js +1 -1
- package/lib/esm/rtc-peer-connection.js +1 -1
- package/lib/esm/rtc-rtp-receiver.js +1 -1
- package/lib/esm/rtc-rtp-sender.js +1 -1
- package/lib/esm/rtc-rtp-transceiver.js +1 -1
- package/lib/esm/rtc-sctp-transport.js +1 -1
- package/lib/esm/rtc-session-description.js +1 -1
- package/lib/esm/rtc-stats-report.js +1 -1
- package/lib/esm/rtc-track-event.js +1 -1
- package/lib/esm/rtp-capabilities.js +1 -1
- package/lib/esm/tee-multiplexer.js +1 -1
- package/lib/esm/wpt-helpers.js +1 -1
- package/lib/types/gst-enum-maps.d.ts +2 -1
- package/lib/types/internal/gst-types.d.ts +83 -0
- package/lib/types/rtc-rtp-sender.d.ts +3 -2
- package/package.json +13 -13
- package/src/gst-enum-maps.ts +1 -1
- package/src/internal/gst-types.ts +122 -0
- package/src/rtc-peer-connection.ts +44 -28
- package/src/rtc-rtp-sender.ts +76 -49
- package/src/rtc-rtp-transceiver.ts +8 -4
- 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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
291
|
+
try { return gstToIceGatheringState(asWebRtcBin(this._webrtcbin).ice_gathering_state); }
|
|
291
292
|
catch { return 'new'; }
|
|
292
293
|
}
|
|
293
294
|
|
|
294
|
-
private _descProp(
|
|
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
|
|
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
|
-
|
|
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 &&
|
|
584
|
+
const hasGstSource = trackOrKind instanceof MediaStreamTrack && trackOrKind._gstSource;
|
|
576
585
|
const wantsSend = direction === 'sendrecv' || direction === 'sendonly';
|
|
577
586
|
|
|
578
|
-
let gstTrans:
|
|
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
|
-
|
|
598
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
633
|
-
|
|
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
|
-
|
|
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():
|
|
813
|
+
private _findNewGstTransceiver(): GstWebRTC.WebRTCRTPTransceiver | null {
|
|
801
814
|
for (let i = 0; ; i++) {
|
|
802
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
1000
|
+
this._iceTransport._setGatheringState(gatheringState);
|
|
985
1001
|
}
|
|
986
1002
|
|
|
987
1003
|
// ---- on<event> attribute handlers --------------------------------------
|
package/src/rtc-rtp-sender.ts
CHANGED
|
@@ -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:
|
|
88
|
-
private _webrtcbin:
|
|
95
|
+
private _pipeline: GstNs.Pipeline | null = null;
|
|
96
|
+
private _webrtcbin: GstNs.Element | null = null;
|
|
89
97
|
private _mlineIndex: number = -1;
|
|
90
|
-
private _elements:
|
|
91
|
-
private _valve:
|
|
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:
|
|
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:
|
|
114
|
+
_onPipelineChanged: ((newPipeline: GstNs.Pipeline) => void) | null = null;
|
|
107
115
|
|
|
108
|
-
constructor(
|
|
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 =
|
|
158
|
+
const source = track._gstSource as GstNs.Element | null;
|
|
147
159
|
if (!source) return; // No GStreamer backing — nothing to wire
|
|
148
160
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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 =
|
|
158
|
-
const tee =
|
|
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 (
|
|
200
|
+
} else if (trackGst._teeMultiplexer) {
|
|
180
201
|
// Track already has a TeeMultiplexer (shared with another PC) — request a new branch
|
|
181
|
-
const tee =
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
220
|
+
trackGst._gstPipeline = this._pipeline;
|
|
200
221
|
|
|
201
222
|
// Create tee in this pipeline
|
|
202
223
|
const tee = new TeeMultiplexer(this._pipeline, source);
|
|
203
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
236
|
-
let lastElement:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
const payloader = Gst.ElementFactory.make('rtpvp8pay', null)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const capsfilter = Gst.ElementFactory.make('capsfilter', null)
|
|
279
|
-
|
|
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)
|
|
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
|
-
|
|
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 =
|
|
421
|
+
const newSource = trackGst._gstSource;
|
|
395
422
|
|
|
396
423
|
// Move new source from its pipeline
|
|
397
|
-
const oldPipeline =
|
|
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
|
-
|
|
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
|
|
433
|
+
if (this._valve) oldSource.unlink(this._valve);
|
|
434
|
+
this._pipeline?.remove(oldSource);
|
|
408
435
|
|
|
409
|
-
this._pipeline
|
|
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 (
|
|
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)
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
57
|
+
this._gstTrans.direction = w3cDirectionToGst(d);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
get currentDirection(): RTCRtpTransceiverDirection | null {
|
|
61
61
|
if (this._stopped) return null;
|
|
62
|
-
|
|
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;
|