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