@gjsify/webrtc 0.1.15

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