@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.
- package/lib/esm/get-user-media.js +93 -0
- package/lib/esm/gst-enum-maps.js +88 -0
- package/lib/esm/gst-init.js +34 -0
- package/lib/esm/gst-stats-parser.js +79 -0
- package/lib/esm/gst-utils.js +16 -0
- package/lib/esm/index.js +53 -0
- package/lib/esm/media-device-info.js +23 -0
- package/lib/esm/media-devices.js +147 -0
- package/lib/esm/media-stream-track.js +142 -0
- package/lib/esm/media-stream.js +78 -0
- package/lib/esm/register/data-channel.js +8 -0
- package/lib/esm/register/error.js +8 -0
- package/lib/esm/register/media-devices.js +7 -0
- package/lib/esm/register/media.js +12 -0
- package/lib/esm/register/peer-connection.js +16 -0
- package/lib/esm/register.js +5 -0
- package/lib/esm/rtc-certificate.js +70 -0
- package/lib/esm/rtc-data-channel.js +266 -0
- package/lib/esm/rtc-dtls-transport.js +41 -0
- package/lib/esm/rtc-dtmf-sender.js +109 -0
- package/lib/esm/rtc-error.js +24 -0
- package/lib/esm/rtc-events.js +35 -0
- package/lib/esm/rtc-ice-candidate.js +75 -0
- package/lib/esm/rtc-ice-transport.js +96 -0
- package/lib/esm/rtc-peer-connection.js +855 -0
- package/lib/esm/rtc-rtp-receiver.js +91 -0
- package/lib/esm/rtc-rtp-sender.js +298 -0
- package/lib/esm/rtc-rtp-transceiver.js +97 -0
- package/lib/esm/rtc-sctp-transport.js +40 -0
- package/lib/esm/rtc-session-description.js +57 -0
- package/lib/esm/rtc-stats-report.js +35 -0
- package/lib/esm/rtc-track-event.js +29 -0
- package/lib/esm/rtp-capabilities.js +41 -0
- package/lib/esm/tee-multiplexer.js +62 -0
- package/lib/esm/wpt-helpers.js +122 -0
- package/lib/types/get-user-media.d.ts +14 -0
- package/lib/types/gst-enum-maps.d.ts +10 -0
- package/lib/types/gst-init.d.ts +5 -0
- package/lib/types/gst-stats-parser.d.ts +16 -0
- package/lib/types/gst-utils.d.ts +11 -0
- package/lib/types/index.d.ts +41 -0
- package/lib/types/media-device-info.d.ts +14 -0
- package/lib/types/media-devices.d.ts +12 -0
- package/lib/types/media-stream-track.d.ts +59 -0
- package/lib/types/media-stream.d.ts +28 -0
- package/lib/types/register/data-channel.d.ts +1 -0
- package/lib/types/register/error.d.ts +1 -0
- package/lib/types/register/media-devices.d.ts +1 -0
- package/lib/types/register/media.d.ts +1 -0
- package/lib/types/register/peer-connection.d.ts +1 -0
- package/lib/types/register.d.ts +5 -0
- package/lib/types/register.spec.d.ts +3 -0
- package/lib/types/rtc-certificate.d.ts +23 -0
- package/lib/types/rtc-data-channel.d.ts +64 -0
- package/lib/types/rtc-dtls-transport.d.ts +20 -0
- package/lib/types/rtc-dtmf-sender.d.ts +31 -0
- package/lib/types/rtc-error.d.ts +19 -0
- package/lib/types/rtc-events.d.ts +27 -0
- package/lib/types/rtc-ice-candidate.d.ts +28 -0
- package/lib/types/rtc-ice-transport.d.ts +56 -0
- package/lib/types/rtc-peer-connection.d.ts +165 -0
- package/lib/types/rtc-rtp-receiver.d.ts +45 -0
- package/lib/types/rtc-rtp-sender.d.ts +98 -0
- package/lib/types/rtc-rtp-transceiver.d.ts +20 -0
- package/lib/types/rtc-sctp-transport.d.ts +20 -0
- package/lib/types/rtc-session-description.d.ts +18 -0
- package/lib/types/rtc-stats-report.d.ts +22 -0
- package/lib/types/rtc-track-event.d.ts +18 -0
- package/lib/types/rtp-capabilities.d.ts +3 -0
- package/lib/types/tee-multiplexer.d.ts +25 -0
- package/lib/types/webrtc.spec.d.ts +2 -0
- package/lib/types/wpt-helpers.d.ts +30 -0
- package/lib/types/wpt-media.spec.d.ts +2 -0
- package/lib/types/wpt.spec.d.ts +2 -0
- package/package.json +74 -0
- package/src/get-user-media.ts +131 -0
- package/src/gst-enum-maps.ts +125 -0
- package/src/gst-init.ts +52 -0
- package/src/gst-stats-parser.ts +137 -0
- package/src/gst-utils.ts +41 -0
- package/src/index.ts +104 -0
- package/src/media-device-info.ts +33 -0
- package/src/media-devices.ts +191 -0
- package/src/media-stream-track.ts +159 -0
- package/src/media-stream.ts +96 -0
- package/src/register/data-channel.ts +11 -0
- package/src/register/error.ts +11 -0
- package/src/register/media-devices.ts +10 -0
- package/src/register/media.ts +15 -0
- package/src/register/peer-connection.ts +20 -0
- package/src/register.spec.ts +55 -0
- package/src/register.ts +10 -0
- package/src/rtc-certificate.ts +110 -0
- package/src/rtc-data-channel.ts +284 -0
- package/src/rtc-dtls-transport.ts +48 -0
- package/src/rtc-dtmf-sender.ts +146 -0
- package/src/rtc-error.ts +49 -0
- package/src/rtc-events.ts +64 -0
- package/src/rtc-ice-candidate.ts +115 -0
- package/src/rtc-ice-transport.ts +104 -0
- package/src/rtc-peer-connection.ts +1017 -0
- package/src/rtc-rtp-receiver.ts +122 -0
- package/src/rtc-rtp-sender.ts +444 -0
- package/src/rtc-rtp-transceiver.ts +127 -0
- package/src/rtc-sctp-transport.ts +48 -0
- package/src/rtc-session-description.ts +64 -0
- package/src/rtc-stats-report.ts +39 -0
- package/src/rtc-track-event.ts +45 -0
- package/src/rtp-capabilities.ts +48 -0
- package/src/tee-multiplexer.ts +75 -0
- package/src/test.mts +11 -0
- package/src/webrtc.spec.ts +1186 -0
- package/src/wpt-helpers.ts +156 -0
- package/src/wpt-media.spec.ts +1154 -0
- package/src/wpt.spec.ts +1136 -0
- package/tsconfig.json +36 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// getUserMedia for GJS — wraps GStreamer source elements as MediaStreamTracks.
|
|
2
|
+
//
|
|
3
|
+
// Phase 3: basic media capture. Tries real audio/video sources first
|
|
4
|
+
// (pipewiresrc, pulsesrc, v4l2src), falls back to test sources.
|
|
5
|
+
// Phase 4.3: constraint support — width, height, frameRate, sampleRate,
|
|
6
|
+
// channelCount mapped to GStreamer capsfilter elements.
|
|
7
|
+
//
|
|
8
|
+
// Reference: W3C Media Capture and Streams spec § 10.3
|
|
9
|
+
|
|
10
|
+
import { Gst, ensureGstInit } from './gst-init.js';
|
|
11
|
+
import { MediaStreamTrack } from './media-stream-track.js';
|
|
12
|
+
import { MediaStream } from './media-stream.js';
|
|
13
|
+
|
|
14
|
+
export interface MediaTrackConstraints {
|
|
15
|
+
deviceId?: string;
|
|
16
|
+
sampleRate?: number;
|
|
17
|
+
channelCount?: number;
|
|
18
|
+
width?: number;
|
|
19
|
+
height?: number;
|
|
20
|
+
frameRate?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MediaStreamConstraints {
|
|
24
|
+
audio?: boolean | MediaTrackConstraints;
|
|
25
|
+
video?: boolean | MediaTrackConstraints;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream> {
|
|
29
|
+
ensureGstInit();
|
|
30
|
+
|
|
31
|
+
if (!constraints.audio && !constraints.video) {
|
|
32
|
+
throw new TypeError(
|
|
33
|
+
"Failed to execute 'getUserMedia': At least one of audio or video must be requested",
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const tracks: MediaStreamTrack[] = [];
|
|
38
|
+
|
|
39
|
+
if (constraints.audio) {
|
|
40
|
+
const audioConstraints = typeof constraints.audio === 'object' ? constraints.audio : {};
|
|
41
|
+
const source = _createAudioSource();
|
|
42
|
+
const pipeline = new Gst.Pipeline() as any;
|
|
43
|
+
pipeline.add(source);
|
|
44
|
+
|
|
45
|
+
// Apply audio constraints via capsfilter
|
|
46
|
+
const capsStr = _buildAudioCaps(audioConstraints);
|
|
47
|
+
if (capsStr) {
|
|
48
|
+
const capsfilter = Gst.ElementFactory.make('capsfilter', null)!;
|
|
49
|
+
(capsfilter as any).caps = Gst.Caps.from_string(capsStr);
|
|
50
|
+
pipeline.add(capsfilter);
|
|
51
|
+
source.link(capsfilter);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
tracks.push(new MediaStreamTrack({
|
|
55
|
+
kind: 'audio',
|
|
56
|
+
label: source.name ?? 'audio',
|
|
57
|
+
_gst: { source, pipeline },
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (constraints.video) {
|
|
62
|
+
const videoConstraints = typeof constraints.video === 'object' ? constraints.video : {};
|
|
63
|
+
const source = _createVideoSource();
|
|
64
|
+
const pipeline = new Gst.Pipeline() as any;
|
|
65
|
+
pipeline.add(source);
|
|
66
|
+
|
|
67
|
+
// Apply video constraints via capsfilter
|
|
68
|
+
const capsStr = _buildVideoCaps(videoConstraints);
|
|
69
|
+
if (capsStr) {
|
|
70
|
+
const capsfilter = Gst.ElementFactory.make('capsfilter', null)!;
|
|
71
|
+
(capsfilter as any).caps = Gst.Caps.from_string(capsStr);
|
|
72
|
+
pipeline.add(capsfilter);
|
|
73
|
+
source.link(capsfilter);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
tracks.push(new MediaStreamTrack({
|
|
77
|
+
kind: 'video',
|
|
78
|
+
label: source.name ?? 'video',
|
|
79
|
+
_gst: { source, pipeline },
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return new MediaStream(tracks);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Build a GStreamer caps string for audio constraints. */
|
|
87
|
+
function _buildAudioCaps(c: MediaTrackConstraints): string | null {
|
|
88
|
+
const parts: string[] = [];
|
|
89
|
+
if (c.sampleRate != null) parts.push(`rate=${Math.trunc(c.sampleRate)}`);
|
|
90
|
+
if (c.channelCount != null) parts.push(`channels=${Math.trunc(c.channelCount)}`);
|
|
91
|
+
if (parts.length === 0) return null;
|
|
92
|
+
return `audio/x-raw,${parts.join(',')}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Build a GStreamer caps string for video constraints. */
|
|
96
|
+
function _buildVideoCaps(c: MediaTrackConstraints): string | null {
|
|
97
|
+
const parts: string[] = [];
|
|
98
|
+
if (c.width != null) parts.push(`width=${Math.trunc(c.width)}`);
|
|
99
|
+
if (c.height != null) parts.push(`height=${Math.trunc(c.height)}`);
|
|
100
|
+
if (c.frameRate != null) parts.push(`framerate=${Math.trunc(c.frameRate)}/1`);
|
|
101
|
+
if (parts.length === 0) return null;
|
|
102
|
+
return `video/x-raw,${parts.join(',')}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function _createAudioSource(): any {
|
|
106
|
+
// Try real sources in priority order
|
|
107
|
+
for (const name of ['pipewiresrc', 'pulsesrc', 'autoaudiosrc']) {
|
|
108
|
+
const el = Gst.ElementFactory.make(name, null);
|
|
109
|
+
if (el) {
|
|
110
|
+
try { (el as any).is_live = true; } catch { /* not all sources have is-live */ }
|
|
111
|
+
return el;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Fallback: test source (sine wave, audible for debugging)
|
|
115
|
+
const el = Gst.ElementFactory.make('audiotestsrc', null)!;
|
|
116
|
+
(el as any).is_live = true;
|
|
117
|
+
(el as any).wave = 0; // sine
|
|
118
|
+
return el;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _createVideoSource(): any {
|
|
122
|
+
for (const name of ['pipewiresrc', 'v4l2src', 'autovideosrc']) {
|
|
123
|
+
const el = Gst.ElementFactory.make(name, null);
|
|
124
|
+
if (el) return el;
|
|
125
|
+
}
|
|
126
|
+
// Fallback: test pattern
|
|
127
|
+
const el = Gst.ElementFactory.make('videotestsrc', null)!;
|
|
128
|
+
(el as any).is_live = true;
|
|
129
|
+
(el as any).pattern = 0; // SMPTE bars
|
|
130
|
+
return el;
|
|
131
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// GStreamer ↔ W3C enum conversions for WebRTC types.
|
|
2
|
+
//
|
|
3
|
+
// Centralises the bidirectional mapping between GstWebRTC C enums and
|
|
4
|
+
// W3C string literals used across RTCPeerConnection and RTCRtpTransceiver.
|
|
5
|
+
|
|
6
|
+
import GstWebRTC from 'gi://GstWebRTC?version=1.0';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
RTCSignalingState,
|
|
10
|
+
RTCPeerConnectionState,
|
|
11
|
+
RTCIceConnectionState,
|
|
12
|
+
RTCIceGatheringState,
|
|
13
|
+
} from './rtc-peer-connection.js';
|
|
14
|
+
import type { RTCRtpTransceiverDirection } from './rtc-rtp-sender.js';
|
|
15
|
+
|
|
16
|
+
// ---- Signaling state --------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
const SIGNALING_STATE_MAP: Record<number, RTCSignalingState> = {
|
|
19
|
+
[GstWebRTC.WebRTCSignalingState.STABLE]: 'stable',
|
|
20
|
+
[GstWebRTC.WebRTCSignalingState.CLOSED]: 'closed',
|
|
21
|
+
[GstWebRTC.WebRTCSignalingState.HAVE_LOCAL_OFFER]: 'have-local-offer',
|
|
22
|
+
[GstWebRTC.WebRTCSignalingState.HAVE_REMOTE_OFFER]: 'have-remote-offer',
|
|
23
|
+
[GstWebRTC.WebRTCSignalingState.HAVE_LOCAL_PRANSWER]: 'have-local-pranswer',
|
|
24
|
+
[GstWebRTC.WebRTCSignalingState.HAVE_REMOTE_PRANSWER]: 'have-remote-pranswer',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export function gstToSignalingState(v: number): RTCSignalingState {
|
|
28
|
+
return SIGNALING_STATE_MAP[v] ?? 'stable';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---- Connection state -------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
const CONNECTION_STATE_MAP: Record<number, RTCPeerConnectionState> = {
|
|
34
|
+
[GstWebRTC.WebRTCPeerConnectionState.NEW]: 'new',
|
|
35
|
+
[GstWebRTC.WebRTCPeerConnectionState.CONNECTING]: 'connecting',
|
|
36
|
+
[GstWebRTC.WebRTCPeerConnectionState.CONNECTED]: 'connected',
|
|
37
|
+
[GstWebRTC.WebRTCPeerConnectionState.DISCONNECTED]: 'disconnected',
|
|
38
|
+
[GstWebRTC.WebRTCPeerConnectionState.FAILED]: 'failed',
|
|
39
|
+
[GstWebRTC.WebRTCPeerConnectionState.CLOSED]: 'closed',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function gstToConnectionState(v: number): RTCPeerConnectionState {
|
|
43
|
+
return CONNECTION_STATE_MAP[v] ?? 'new';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---- ICE connection state ---------------------------------------------------
|
|
47
|
+
|
|
48
|
+
const ICE_CONNECTION_STATE_MAP: Record<number, RTCIceConnectionState> = {
|
|
49
|
+
[GstWebRTC.WebRTCICEConnectionState.NEW]: 'new',
|
|
50
|
+
[GstWebRTC.WebRTCICEConnectionState.CHECKING]: 'checking',
|
|
51
|
+
[GstWebRTC.WebRTCICEConnectionState.CONNECTED]: 'connected',
|
|
52
|
+
[GstWebRTC.WebRTCICEConnectionState.COMPLETED]: 'completed',
|
|
53
|
+
[GstWebRTC.WebRTCICEConnectionState.FAILED]: 'failed',
|
|
54
|
+
[GstWebRTC.WebRTCICEConnectionState.DISCONNECTED]: 'disconnected',
|
|
55
|
+
[GstWebRTC.WebRTCICEConnectionState.CLOSED]: 'closed',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function gstToIceConnectionState(v: number): RTCIceConnectionState {
|
|
59
|
+
return ICE_CONNECTION_STATE_MAP[v] ?? 'new';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---- ICE gathering state ----------------------------------------------------
|
|
63
|
+
|
|
64
|
+
const ICE_GATHERING_STATE_MAP: Record<number, RTCIceGatheringState> = {
|
|
65
|
+
[GstWebRTC.WebRTCICEGatheringState.NEW]: 'new',
|
|
66
|
+
[GstWebRTC.WebRTCICEGatheringState.GATHERING]: 'gathering',
|
|
67
|
+
[GstWebRTC.WebRTCICEGatheringState.COMPLETE]: 'complete',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function gstToIceGatheringState(v: number): RTCIceGatheringState {
|
|
71
|
+
return ICE_GATHERING_STATE_MAP[v] ?? 'new';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- Transceiver direction (bidirectional) ----------------------------------
|
|
75
|
+
|
|
76
|
+
const DIRECTION_GST_TO_W3C: Record<number, RTCRtpTransceiverDirection> = {
|
|
77
|
+
[GstWebRTC.WebRTCRTPTransceiverDirection.SENDRECV]: 'sendrecv',
|
|
78
|
+
[GstWebRTC.WebRTCRTPTransceiverDirection.SENDONLY]: 'sendonly',
|
|
79
|
+
[GstWebRTC.WebRTCRTPTransceiverDirection.RECVONLY]: 'recvonly',
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const DIRECTION_W3C_TO_GST: Record<string, number> = {
|
|
83
|
+
sendrecv: GstWebRTC.WebRTCRTPTransceiverDirection.SENDRECV,
|
|
84
|
+
sendonly: GstWebRTC.WebRTCRTPTransceiverDirection.SENDONLY,
|
|
85
|
+
recvonly: GstWebRTC.WebRTCRTPTransceiverDirection.RECVONLY,
|
|
86
|
+
inactive: GstWebRTC.WebRTCRTPTransceiverDirection.NONE,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export function gstDirectionToW3C(v: number): RTCRtpTransceiverDirection {
|
|
90
|
+
return DIRECTION_GST_TO_W3C[v] ?? 'inactive';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function w3cDirectionToGst(d: RTCRtpTransceiverDirection): number {
|
|
94
|
+
return DIRECTION_W3C_TO_GST[d] ?? GstWebRTC.WebRTCRTPTransceiverDirection.NONE;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ---- Stats type (GstWebRTC → W3C RTCStatsType string) -----------------------
|
|
98
|
+
|
|
99
|
+
export type RTCStatsType =
|
|
100
|
+
| 'codec' | 'inbound-rtp' | 'outbound-rtp'
|
|
101
|
+
| 'remote-inbound-rtp' | 'remote-outbound-rtp'
|
|
102
|
+
| 'csrc' | 'peer-connection' | 'data-channel'
|
|
103
|
+
| 'stream' | 'transport' | 'candidate-pair'
|
|
104
|
+
| 'local-candidate' | 'remote-candidate' | 'certificate';
|
|
105
|
+
|
|
106
|
+
const STATS_TYPE_MAP: Record<number, RTCStatsType> = {
|
|
107
|
+
[GstWebRTC.WebRTCStatsType.CODEC]: 'codec',
|
|
108
|
+
[GstWebRTC.WebRTCStatsType.INBOUND_RTP]: 'inbound-rtp',
|
|
109
|
+
[GstWebRTC.WebRTCStatsType.OUTBOUND_RTP]: 'outbound-rtp',
|
|
110
|
+
[GstWebRTC.WebRTCStatsType.REMOTE_INBOUND_RTP]: 'remote-inbound-rtp',
|
|
111
|
+
[GstWebRTC.WebRTCStatsType.REMOTE_OUTBOUND_RTP]: 'remote-outbound-rtp',
|
|
112
|
+
[GstWebRTC.WebRTCStatsType.CSRC]: 'csrc',
|
|
113
|
+
[GstWebRTC.WebRTCStatsType.PEER_CONNECTION]: 'peer-connection',
|
|
114
|
+
[GstWebRTC.WebRTCStatsType.DATA_CHANNEL]: 'data-channel',
|
|
115
|
+
[GstWebRTC.WebRTCStatsType.STREAM]: 'stream',
|
|
116
|
+
[GstWebRTC.WebRTCStatsType.TRANSPORT]: 'transport',
|
|
117
|
+
[GstWebRTC.WebRTCStatsType.CANDIDATE_PAIR]: 'candidate-pair',
|
|
118
|
+
[GstWebRTC.WebRTCStatsType.LOCAL_CANDIDATE]: 'local-candidate',
|
|
119
|
+
[GstWebRTC.WebRTCStatsType.REMOTE_CANDIDATE]: 'remote-candidate',
|
|
120
|
+
[GstWebRTC.WebRTCStatsType.CERTIFICATE]: 'certificate',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export function gstToStatsType(v: number): RTCStatsType | undefined {
|
|
124
|
+
return STATS_TYPE_MAP[v];
|
|
125
|
+
}
|
package/src/gst-init.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Lazy GStreamer initialization for the WebRTC backend.
|
|
2
|
+
//
|
|
3
|
+
// `webrtcbin` ships with GStreamer's gst-plugins-bad (`libgstwebrtc.so`).
|
|
4
|
+
// On Fedora: gstreamer1-plugins-bad-free + gstreamer1-plugins-bad-free-extras.
|
|
5
|
+
// On Ubuntu/Debian: gstreamer1.0-plugins-bad + gstreamer1.0-nice.
|
|
6
|
+
//
|
|
7
|
+
// This module is GJS-only — the Node alias layer routes it to @gjsify/empty.
|
|
8
|
+
|
|
9
|
+
import Gst from 'gi://Gst?version=1.0';
|
|
10
|
+
|
|
11
|
+
let initialized = false;
|
|
12
|
+
|
|
13
|
+
export function ensureGstInit(): void {
|
|
14
|
+
if (initialized) return;
|
|
15
|
+
Gst.init(null);
|
|
16
|
+
initialized = true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Throws if the `webrtcbin` element is not registered (gst-plugins-bad missing). */
|
|
20
|
+
export function ensureWebrtcbinAvailable(): void {
|
|
21
|
+
ensureGstInit();
|
|
22
|
+
const webrtcFactory = Gst.ElementFactory.find('webrtcbin');
|
|
23
|
+
if (!webrtcFactory) {
|
|
24
|
+
throwNotSupported(
|
|
25
|
+
'GStreamer element "webrtcbin" not available. Install gst-plugins-bad:\n' +
|
|
26
|
+
' Fedora: dnf install gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-extras\n' +
|
|
27
|
+
' Ubuntu/Debian: apt install gstreamer1.0-plugins-bad',
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
// webrtcbin requires libnice's GStreamer plugin for ICE transport —
|
|
31
|
+
// without it, pipeline state-change to PLAYING fails and createDataChannel
|
|
32
|
+
// hits the "webrtc->priv->is_closed" assertion.
|
|
33
|
+
const niceFactory = Gst.ElementFactory.find('nicesrc');
|
|
34
|
+
if (!niceFactory) {
|
|
35
|
+
throwNotSupported(
|
|
36
|
+
'GStreamer "nice" plugin (libnice-gstreamer) not available — required by webrtcbin.\n' +
|
|
37
|
+
' Fedora: dnf install libnice-gstreamer1\n' +
|
|
38
|
+
' Ubuntu/Debian: apt install gstreamer1.0-nice\n' +
|
|
39
|
+
' Verify with: gst-inspect-1.0 nicesrc',
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function throwNotSupported(message: string): never {
|
|
45
|
+
const DOMExceptionCtor: typeof DOMException | undefined = (globalThis as any).DOMException;
|
|
46
|
+
if (DOMExceptionCtor) {
|
|
47
|
+
throw new DOMExceptionCtor(message, 'NotSupportedError');
|
|
48
|
+
}
|
|
49
|
+
throw new Error(message);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { Gst };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// GstStructure → W3C RTCStatsReport parser.
|
|
2
|
+
//
|
|
3
|
+
// webrtcbin's `get-stats` action signal returns a GstStructure where each
|
|
4
|
+
// field is a nested GstStructure containing one stats entry. Each entry
|
|
5
|
+
// has a `type` field (GstWebRTCStatsType enum), an `id` field, a `timestamp`
|
|
6
|
+
// field, plus type-specific fields in GStreamer snake_case naming.
|
|
7
|
+
//
|
|
8
|
+
// This module iterates the top-level fields, converts each nested structure
|
|
9
|
+
// to a W3C-compliant stats dictionary with camelCase keys.
|
|
10
|
+
|
|
11
|
+
import type Gst from 'gi://Gst?version=1.0';
|
|
12
|
+
|
|
13
|
+
import { gstToStatsType } from './gst-enum-maps.js';
|
|
14
|
+
import { RTCStatsReport, type RTCStats } from './rtc-stats-report.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert a GStreamer snake_case field name to W3C camelCase.
|
|
18
|
+
* Examples: `bytes-sent` → `bytesSent`, `packets-received` → `packetsReceived`
|
|
19
|
+
*/
|
|
20
|
+
function snakeToCamel(name: string): string {
|
|
21
|
+
return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Extract all fields from a GstStructure into a plain object.
|
|
26
|
+
* GJS unboxes `get_value` for boxed/simple types directly — no
|
|
27
|
+
* GObject.Value wrapper is returned for primitive types.
|
|
28
|
+
*/
|
|
29
|
+
function extractFields(structure: Gst.Structure): Record<string, unknown> {
|
|
30
|
+
const result: Record<string, unknown> = {};
|
|
31
|
+
const n = structure.n_fields();
|
|
32
|
+
for (let i = 0; i < n; i++) {
|
|
33
|
+
const fieldName = structure.nth_field_name(i);
|
|
34
|
+
try {
|
|
35
|
+
const value = structure.get_value(fieldName);
|
|
36
|
+
result[fieldName] = value;
|
|
37
|
+
} catch {
|
|
38
|
+
// Some fields may not be extractable — skip silently
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parse a single stats entry (nested GstStructure) into a W3C RTCStats object.
|
|
46
|
+
*/
|
|
47
|
+
function parseStatsEntry(structure: Gst.Structure): RTCStats | null {
|
|
48
|
+
const raw = extractFields(structure);
|
|
49
|
+
|
|
50
|
+
// The `type` field is a GstWebRTCStatsType enum value
|
|
51
|
+
const gstType = raw['type'];
|
|
52
|
+
if (gstType == null) return null;
|
|
53
|
+
const w3cType = gstToStatsType(Number(gstType));
|
|
54
|
+
if (!w3cType) return null;
|
|
55
|
+
|
|
56
|
+
const id = String(raw['id'] ?? structure.get_name() ?? '');
|
|
57
|
+
// GStreamer provides timestamp in nanoseconds (pipeline clock).
|
|
58
|
+
// Convert to milliseconds for W3C (performance.now()-relative).
|
|
59
|
+
const tsRaw = raw['timestamp'];
|
|
60
|
+
const timestamp = tsRaw != null ? Number(tsRaw) / 1_000_000 : performance.now();
|
|
61
|
+
|
|
62
|
+
const stats: RTCStats = { type: w3cType, id, timestamp };
|
|
63
|
+
|
|
64
|
+
// Copy remaining fields, converting snake_case → camelCase
|
|
65
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
66
|
+
if (key === 'type' || key === 'id' || key === 'timestamp') continue;
|
|
67
|
+
const camelKey = snakeToCamel(key);
|
|
68
|
+
stats[camelKey] = value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return stats;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parse the top-level GstStructure returned by webrtcbin's `get-stats`
|
|
76
|
+
* signal into a W3C RTCStatsReport.
|
|
77
|
+
*
|
|
78
|
+
* The top-level structure has one field per stats entry. Each field's value
|
|
79
|
+
* is a nested GstStructure (GJS unboxes boxed types automatically).
|
|
80
|
+
*/
|
|
81
|
+
export function parseGstStats(reply: Gst.Structure | null): RTCStatsReport {
|
|
82
|
+
if (!reply) return new RTCStatsReport();
|
|
83
|
+
|
|
84
|
+
const entries: Array<[string, RTCStats]> = [];
|
|
85
|
+
const n = reply.n_fields();
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < n; i++) {
|
|
88
|
+
const fieldName = reply.nth_field_name(i);
|
|
89
|
+
try {
|
|
90
|
+
const nested = reply.get_value(fieldName);
|
|
91
|
+
// GJS unboxes the nested GstStructure directly
|
|
92
|
+
if (nested && typeof (nested as any).n_fields === 'function') {
|
|
93
|
+
const stats = parseStatsEntry(nested as unknown as Gst.Structure);
|
|
94
|
+
if (stats) {
|
|
95
|
+
entries.push([stats.id, stats]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Non-structure fields (e.g. metadata) — skip
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return new RTCStatsReport(entries);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Filter an RTCStatsReport to entries relevant to a specific track identifier.
|
|
108
|
+
* Used by RTCRtpSender.getStats() and RTCRtpReceiver.getStats() to return
|
|
109
|
+
* only the stats associated with their track.
|
|
110
|
+
*/
|
|
111
|
+
export function filterStatsByTrackId(report: RTCStatsReport, trackId: string): RTCStatsReport {
|
|
112
|
+
const entries: Array<[string, RTCStats]> = [];
|
|
113
|
+
const relatedIds = new Set<string>();
|
|
114
|
+
|
|
115
|
+
// First pass: find stats entries that reference this track
|
|
116
|
+
for (const [id, stats] of report) {
|
|
117
|
+
if (stats.trackIdentifier === trackId) {
|
|
118
|
+
entries.push([id, stats]);
|
|
119
|
+
relatedIds.add(id);
|
|
120
|
+
// Collect referenced IDs (transportId, codecId, remoteId, etc.)
|
|
121
|
+
for (const value of Object.values(stats)) {
|
|
122
|
+
if (typeof value === 'string' && report.has(value)) {
|
|
123
|
+
relatedIds.add(value);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Second pass: include referenced stats (codec, transport, candidate-pair, etc.)
|
|
130
|
+
for (const [id, stats] of report) {
|
|
131
|
+
if (relatedIds.has(id) && !entries.some(([entryId]) => entryId === id)) {
|
|
132
|
+
entries.push([id, stats]);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return new RTCStatsReport(entries);
|
|
137
|
+
}
|
package/src/gst-utils.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Async bridging between GstPromise and JavaScript Promises.
|
|
2
|
+
//
|
|
3
|
+
// Reference: refs/node-gst-webrtc/src/gstUtils.ts (ISC, Ratchanan Srirattanamet)
|
|
4
|
+
//
|
|
5
|
+
// Why a native bridge? Gst.Promise.new_with_change_func() invokes its
|
|
6
|
+
// callback on GStreamer's internal streaming thread. GJS blocks any JS
|
|
7
|
+
// callback invoked from a non-main thread (to prevent SpiderMonkey VM
|
|
8
|
+
// corruption), so the change_func is never delivered to JS — the Promise
|
|
9
|
+
// would hang forever.
|
|
10
|
+
//
|
|
11
|
+
// `@gjsify/webrtc-native/PromiseBridge` is a Vala helper that registers
|
|
12
|
+
// the change_func on the C side, hops through `g_main_context_invoke()`
|
|
13
|
+
// to the GLib main thread, and only then emits `replied` / `rejected`
|
|
14
|
+
// signals which JS can safely consume.
|
|
15
|
+
|
|
16
|
+
import type Gst from 'gi://Gst?version=1.0';
|
|
17
|
+
import { PromiseBridge } from '@gjsify/webrtc-native';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Wrap a GstPromise-consuming emit into a JS Promise that resolves on the
|
|
21
|
+
* main thread.
|
|
22
|
+
*
|
|
23
|
+
* Usage:
|
|
24
|
+
* const reply = await withGstPromise((promise) => {
|
|
25
|
+
* webrtcbin.emit('create-offer', options, promise);
|
|
26
|
+
* });
|
|
27
|
+
*/
|
|
28
|
+
export function withGstPromise(
|
|
29
|
+
emit: (promise: Gst.Promise) => void,
|
|
30
|
+
): Promise<Gst.Structure | null> {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const bridge = new PromiseBridge();
|
|
33
|
+
bridge.connect('replied', (_b: unknown, reply: Gst.Structure | null) => {
|
|
34
|
+
resolve(reply);
|
|
35
|
+
});
|
|
36
|
+
bridge.connect('rejected', (_b: unknown, message: string) => {
|
|
37
|
+
reject(new Error(message));
|
|
38
|
+
});
|
|
39
|
+
emit(bridge.promise);
|
|
40
|
+
});
|
|
41
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// W3C WebRTC API for GJS — backed by GStreamer webrtcbin.
|
|
2
|
+
//
|
|
3
|
+
// This module has no side effects. Importing @gjsify/webrtc gives named
|
|
4
|
+
// access to the classes but does NOT register globals. Use
|
|
5
|
+
// @gjsify/webrtc/register (or a granular subpath) to set globalThis.RTCPeerConnection etc.
|
|
6
|
+
|
|
7
|
+
export { RTCPeerConnection } from './rtc-peer-connection.js';
|
|
8
|
+
export type {
|
|
9
|
+
RTCConfiguration,
|
|
10
|
+
RTCIceServer,
|
|
11
|
+
RTCOfferOptions,
|
|
12
|
+
RTCAnswerOptions,
|
|
13
|
+
RTCDataChannelInit,
|
|
14
|
+
RTCSignalingState,
|
|
15
|
+
RTCPeerConnectionState,
|
|
16
|
+
RTCIceConnectionState,
|
|
17
|
+
RTCIceGatheringState,
|
|
18
|
+
RTCIceTransportPolicy,
|
|
19
|
+
RTCBundlePolicy,
|
|
20
|
+
RTCRtcpMuxPolicy,
|
|
21
|
+
} from './rtc-peer-connection.js';
|
|
22
|
+
|
|
23
|
+
export { RTCDataChannel } from './rtc-data-channel.js';
|
|
24
|
+
export type { RTCDataChannelState, BinaryType } from './rtc-data-channel.js';
|
|
25
|
+
|
|
26
|
+
export { RTCSessionDescription } from './rtc-session-description.js';
|
|
27
|
+
export type { RTCSessionDescriptionInit, RTCSdpType } from './rtc-session-description.js';
|
|
28
|
+
|
|
29
|
+
export { RTCIceCandidate } from './rtc-ice-candidate.js';
|
|
30
|
+
export type {
|
|
31
|
+
RTCIceCandidateInit,
|
|
32
|
+
RTCIceComponent,
|
|
33
|
+
RTCIceProtocol,
|
|
34
|
+
RTCIceCandidateType,
|
|
35
|
+
RTCIceTcpCandidateType,
|
|
36
|
+
} from './rtc-ice-candidate.js';
|
|
37
|
+
|
|
38
|
+
export { RTCError } from './rtc-error.js';
|
|
39
|
+
export type { RTCErrorInit, RTCErrorDetailType } from './rtc-error.js';
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
RTCPeerConnectionIceEvent,
|
|
43
|
+
RTCDataChannelEvent,
|
|
44
|
+
RTCErrorEvent,
|
|
45
|
+
} from './rtc-events.js';
|
|
46
|
+
export type {
|
|
47
|
+
RTCPeerConnectionIceEventInit,
|
|
48
|
+
RTCDataChannelEventInit,
|
|
49
|
+
RTCErrorEventInit,
|
|
50
|
+
} from './rtc-events.js';
|
|
51
|
+
|
|
52
|
+
export { RTCRtpSender } from './rtc-rtp-sender.js';
|
|
53
|
+
export type {
|
|
54
|
+
RTCRtpTransceiverDirection,
|
|
55
|
+
RTCRtpCapabilities,
|
|
56
|
+
RTCRtpCodecCapability,
|
|
57
|
+
RTCRtpHeaderExtensionCapability,
|
|
58
|
+
RTCRtpSendParameters,
|
|
59
|
+
RTCRtpEncodingParameters,
|
|
60
|
+
RTCRtpCodecParameters,
|
|
61
|
+
RTCRtpHeaderExtensionParameters,
|
|
62
|
+
RTCRtcpParameters,
|
|
63
|
+
} from './rtc-rtp-sender.js';
|
|
64
|
+
|
|
65
|
+
export { RTCRtpReceiver } from './rtc-rtp-receiver.js';
|
|
66
|
+
export type { RTCRtpReceiveParameters } from './rtc-rtp-receiver.js';
|
|
67
|
+
|
|
68
|
+
export { RTCRtpTransceiver } from './rtc-rtp-transceiver.js';
|
|
69
|
+
|
|
70
|
+
export { MediaStream } from './media-stream.js';
|
|
71
|
+
export { MediaStreamTrackEvent } from './media-stream.js';
|
|
72
|
+
|
|
73
|
+
export { MediaStreamTrack } from './media-stream-track.js';
|
|
74
|
+
export type { MediaStreamTrackInit } from './media-stream-track.js';
|
|
75
|
+
|
|
76
|
+
export { RTCTrackEvent } from './rtc-track-event.js';
|
|
77
|
+
export type { RTCTrackEventInit } from './rtc-track-event.js';
|
|
78
|
+
|
|
79
|
+
export type { RTCRtpTransceiverInit } from './rtc-peer-connection.js';
|
|
80
|
+
|
|
81
|
+
export { getUserMedia } from './get-user-media.js';
|
|
82
|
+
export type { MediaStreamConstraints, MediaTrackConstraints } from './get-user-media.js';
|
|
83
|
+
|
|
84
|
+
export { MediaDevices } from './media-devices.js';
|
|
85
|
+
export { MediaDeviceInfo } from './media-device-info.js';
|
|
86
|
+
export type { MediaDeviceKind } from './media-device-info.js';
|
|
87
|
+
|
|
88
|
+
export { RTCStatsReport } from './rtc-stats-report.js';
|
|
89
|
+
export type { RTCStats } from './rtc-stats-report.js';
|
|
90
|
+
|
|
91
|
+
export { RTCDtlsTransport } from './rtc-dtls-transport.js';
|
|
92
|
+
export type { RTCDtlsTransportState } from './rtc-dtls-transport.js';
|
|
93
|
+
|
|
94
|
+
export { RTCIceTransport } from './rtc-ice-transport.js';
|
|
95
|
+
export type { RTCIceTransportState, RTCIceRole, RTCIceParameters, RTCIceCandidatePair } from './rtc-ice-transport.js';
|
|
96
|
+
|
|
97
|
+
export { RTCSctpTransport } from './rtc-sctp-transport.js';
|
|
98
|
+
export type { RTCSctpTransportState } from './rtc-sctp-transport.js';
|
|
99
|
+
|
|
100
|
+
export { RTCDTMFSender, RTCDTMFToneChangeEvent } from './rtc-dtmf-sender.js';
|
|
101
|
+
export type { RTCDTMFToneChangeEventInit } from './rtc-dtmf-sender.js';
|
|
102
|
+
|
|
103
|
+
export { RTCCertificate } from './rtc-certificate.js';
|
|
104
|
+
export type { RTCDtlsFingerprint, AlgorithmIdentifier } from './rtc-certificate.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// W3C MediaDeviceInfo for GJS — backed by GStreamer Device Monitor.
|
|
2
|
+
//
|
|
3
|
+
// Reference: W3C Media Capture and Streams spec § 10.2.1
|
|
4
|
+
|
|
5
|
+
export type MediaDeviceKind = 'audioinput' | 'audiooutput' | 'videoinput';
|
|
6
|
+
|
|
7
|
+
export class MediaDeviceInfo {
|
|
8
|
+
readonly deviceId: string;
|
|
9
|
+
readonly kind: MediaDeviceKind;
|
|
10
|
+
readonly label: string;
|
|
11
|
+
readonly groupId: string;
|
|
12
|
+
|
|
13
|
+
constructor(init: {
|
|
14
|
+
deviceId: string;
|
|
15
|
+
kind: MediaDeviceKind;
|
|
16
|
+
label: string;
|
|
17
|
+
groupId?: string;
|
|
18
|
+
}) {
|
|
19
|
+
this.deviceId = init.deviceId;
|
|
20
|
+
this.kind = init.kind;
|
|
21
|
+
this.label = init.label;
|
|
22
|
+
this.groupId = init.groupId ?? '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toJSON(): object {
|
|
26
|
+
return {
|
|
27
|
+
deviceId: this.deviceId,
|
|
28
|
+
kind: this.kind,
|
|
29
|
+
label: this.label,
|
|
30
|
+
groupId: this.groupId,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
}
|