@gjsify/webrtc 0.3.21 → 0.4.3
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 +73 -70
- package/src/get-user-media.ts +0 -131
- package/src/gst-enum-maps.ts +0 -125
- package/src/gst-init.ts +0 -49
- package/src/gst-stats-parser.ts +0 -137
- package/src/gst-utils.ts +0 -41
- package/src/index.ts +0 -104
- package/src/media-device-info.ts +0 -33
- package/src/media-devices.ts +0 -191
- package/src/media-stream-track.ts +0 -159
- package/src/media-stream.ts +0 -96
- package/src/register/data-channel.ts +0 -11
- package/src/register/error.ts +0 -11
- package/src/register/media-devices.ts +0 -10
- package/src/register/media.ts +0 -15
- package/src/register/peer-connection.ts +0 -20
- package/src/register.spec.ts +0 -55
- package/src/register.ts +0 -10
- package/src/rtc-certificate.ts +0 -110
- package/src/rtc-data-channel.ts +0 -283
- package/src/rtc-dtls-transport.ts +0 -48
- package/src/rtc-dtmf-sender.ts +0 -146
- package/src/rtc-error.ts +0 -49
- package/src/rtc-events.ts +0 -64
- package/src/rtc-ice-candidate.ts +0 -115
- package/src/rtc-ice-transport.ts +0 -104
- package/src/rtc-peer-connection.ts +0 -1023
- package/src/rtc-rtp-receiver.ts +0 -122
- package/src/rtc-rtp-sender.ts +0 -444
- package/src/rtc-rtp-transceiver.ts +0 -127
- package/src/rtc-sctp-transport.ts +0 -48
- package/src/rtc-session-description.ts +0 -64
- package/src/rtc-stats-report.ts +0 -39
- package/src/rtc-track-event.ts +0 -45
- package/src/rtp-capabilities.ts +0 -48
- package/src/tee-multiplexer.ts +0 -75
- package/src/test.mts +0 -11
- package/src/webrtc.spec.ts +0 -1186
- package/src/wpt-helpers.ts +0 -156
- package/src/wpt-media.spec.ts +0 -1154
- package/src/wpt.spec.ts +0 -1136
- package/tsconfig.json +0 -36
- package/tsconfig.tsbuildinfo +0 -1
package/src/rtc-rtp-receiver.ts
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// W3C RTCRtpReceiver for GJS.
|
|
2
|
-
//
|
|
3
|
-
// Wraps GstWebRTC.WebRTCRTPReceiver. Phase 2.5: incoming media pipeline
|
|
4
|
-
// is managed by ReceiverBridge (Vala) which handles decodebin's
|
|
5
|
-
// streaming-thread signals natively and emits media-flowing on the
|
|
6
|
-
// main thread when decoded media replaces the muted source.
|
|
7
|
-
//
|
|
8
|
-
// Reference: refs/node-gst-webrtc/src/webrtc/RTCRtpReceiver.ts (ISC)
|
|
9
|
-
// Reference: W3C WebRTC spec § 5.3
|
|
10
|
-
|
|
11
|
-
import type GstWebRTC from 'gi://GstWebRTC?version=1.0';
|
|
12
|
-
import type Gst from 'gi://Gst?version=1.0';
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
ReceiverBridge,
|
|
16
|
-
type ReceiverBridge as ReceiverBridgeType,
|
|
17
|
-
} from '@gjsify/webrtc-native';
|
|
18
|
-
|
|
19
|
-
import { MediaStreamTrack } from './media-stream-track.js';
|
|
20
|
-
import { getRtpCapabilities } from './rtp-capabilities.js';
|
|
21
|
-
import type { RTCStatsReport } from './rtc-stats-report.js';
|
|
22
|
-
import type { RTCDtlsTransport } from './rtc-dtls-transport.js';
|
|
23
|
-
import type { RTCRtpCapabilities, RTCRtpCodecParameters, RTCRtpHeaderExtensionParameters, RTCRtcpParameters } from './rtc-rtp-sender.js';
|
|
24
|
-
|
|
25
|
-
export interface RTCRtpReceiveParameters {
|
|
26
|
-
codecs: RTCRtpCodecParameters[];
|
|
27
|
-
headerExtensions: RTCRtpHeaderExtensionParameters[];
|
|
28
|
-
rtcp: RTCRtcpParameters;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface RTCRtpContributingSource {
|
|
32
|
-
timestamp: number;
|
|
33
|
-
source: number;
|
|
34
|
-
audioLevel?: number;
|
|
35
|
-
rtpTimestamp: number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface RTCRtpSynchronizationSource extends RTCRtpContributingSource {
|
|
39
|
-
voiceActivityFlag?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const MAX_JITTER_BUFFER_TARGET = 4000;
|
|
43
|
-
|
|
44
|
-
export class RTCRtpReceiver {
|
|
45
|
-
private _gstReceiver: GstWebRTC.WebRTCRTPReceiver | null;
|
|
46
|
-
private _track: MediaStreamTrack;
|
|
47
|
-
private _jitterBufferTarget: number | null = null;
|
|
48
|
-
private _pipeline: any = null;
|
|
49
|
-
private _receiverBridge: ReceiverBridgeType | null = null;
|
|
50
|
-
/** @internal — stats callback set by RTCPeerConnection */
|
|
51
|
-
_getStatsForTrack: ((track: MediaStreamTrack) => Promise<RTCStatsReport>) | null = null;
|
|
52
|
-
|
|
53
|
-
constructor(kind: 'audio' | 'video', gstReceiver: GstWebRTC.WebRTCRTPReceiver | null, pipeline?: any) {
|
|
54
|
-
this._gstReceiver = gstReceiver;
|
|
55
|
-
this._pipeline = pipeline ?? null;
|
|
56
|
-
this._track = new MediaStreamTrack({ kind, muted: true });
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** @internal — called from RTCPeerConnection._handlePadAdded */
|
|
60
|
-
_connectToPad(pad: Gst.Pad): void {
|
|
61
|
-
if (!this._pipeline || this._receiverBridge) return;
|
|
62
|
-
this._receiverBridge = new (ReceiverBridge as any)({
|
|
63
|
-
pipeline: this._pipeline,
|
|
64
|
-
kind: this._track.kind,
|
|
65
|
-
});
|
|
66
|
-
this._receiverBridge!.connect_to_pad(pad);
|
|
67
|
-
this._receiverBridge!.connect('media-flowing', () => {
|
|
68
|
-
this._track._setMuted(false);
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/** @internal — called from RTCPeerConnection.close() */
|
|
73
|
-
_dispose(): void {
|
|
74
|
-
try { this._receiverBridge?.dispose_bridge(); } catch { /* ignore */ }
|
|
75
|
-
this._receiverBridge = null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** @internal — set by RTCPeerConnection */
|
|
79
|
-
_transport: RTCDtlsTransport | null = null;
|
|
80
|
-
|
|
81
|
-
get track(): MediaStreamTrack { return this._track; }
|
|
82
|
-
get transport(): RTCDtlsTransport | null { return this._transport; }
|
|
83
|
-
|
|
84
|
-
get jitterBufferTarget(): number | null { return this._jitterBufferTarget; }
|
|
85
|
-
set jitterBufferTarget(v: number | null) {
|
|
86
|
-
if (v === null) {
|
|
87
|
-
this._jitterBufferTarget = null;
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const n = Number(v);
|
|
91
|
-
if (!Number.isFinite(n) || n < 0) {
|
|
92
|
-
throw new RangeError(`Failed to set jitterBufferTarget: ${v} is negative or not finite`);
|
|
93
|
-
}
|
|
94
|
-
if (n > MAX_JITTER_BUFFER_TARGET) {
|
|
95
|
-
throw new RangeError(`Failed to set jitterBufferTarget: ${v} exceeds maximum of ${MAX_JITTER_BUFFER_TARGET}`);
|
|
96
|
-
}
|
|
97
|
-
this._jitterBufferTarget = n;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
getParameters(): RTCRtpReceiveParameters {
|
|
101
|
-
return {
|
|
102
|
-
codecs: [],
|
|
103
|
-
headerExtensions: [],
|
|
104
|
-
rtcp: {},
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
getContributingSources(): RTCRtpContributingSource[] { return []; }
|
|
109
|
-
getSynchronizationSources(): RTCRtpSynchronizationSource[] { return []; }
|
|
110
|
-
|
|
111
|
-
async getStats(): Promise<RTCStatsReport> {
|
|
112
|
-
if (this._getStatsForTrack && this._track) {
|
|
113
|
-
return this._getStatsForTrack(this._track);
|
|
114
|
-
}
|
|
115
|
-
const { RTCStatsReport: Report } = await import('./rtc-stats-report.js');
|
|
116
|
-
return new Report();
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
static getCapabilities(kind: string): RTCRtpCapabilities | null {
|
|
120
|
-
return getRtpCapabilities(kind);
|
|
121
|
-
}
|
|
122
|
-
}
|
package/src/rtc-rtp-sender.ts
DELETED
|
@@ -1,444 +0,0 @@
|
|
|
1
|
-
// W3C RTCRtpSender for GJS.
|
|
2
|
-
//
|
|
3
|
-
// Phase 2: API surface wrapping GstWebRTC.WebRTCRTPSender.
|
|
4
|
-
// Phase 3: outgoing media pipeline — builds explicit encoder chains
|
|
5
|
-
// (source → valve → convert → encode → payloader → webrtcbin sink pad)
|
|
6
|
-
// entirely on the main thread. No Vala bridge needed.
|
|
7
|
-
//
|
|
8
|
-
// Reference: refs/node-gst-webrtc/src/webrtc/RTCRtpSender.ts (ISC)
|
|
9
|
-
// Reference: W3C WebRTC spec § 5.2
|
|
10
|
-
|
|
11
|
-
import type 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
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
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
|
-
}
|