@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,191 @@
|
|
|
1
|
+
// W3C MediaDevices for GJS.
|
|
2
|
+
//
|
|
3
|
+
// Phase 3: getUserMedia via GStreamer sources.
|
|
4
|
+
// Phase 4.3: enumerateDevices via GStreamer Device Monitor,
|
|
5
|
+
// getSupportedConstraints returns supported constraints.
|
|
6
|
+
//
|
|
7
|
+
// Reference: W3C Media Capture and Streams spec § 10.2
|
|
8
|
+
// Reference: refs/webkit/Source/WebCore/platform/mediastream/gstreamer/GStreamerCaptureDeviceManager.cpp
|
|
9
|
+
|
|
10
|
+
import '@gjsify/dom-events/register/event-target';
|
|
11
|
+
|
|
12
|
+
import { ensureGstInit, Gst } from './gst-init.js';
|
|
13
|
+
import { getUserMedia, type MediaStreamConstraints } from './get-user-media.js';
|
|
14
|
+
import { MediaDeviceInfo, type MediaDeviceKind } from './media-device-info.js';
|
|
15
|
+
import type { MediaStream } from './media-stream.js';
|
|
16
|
+
|
|
17
|
+
/** Map GStreamer device class strings to W3C MediaDeviceKind. */
|
|
18
|
+
const DEVICE_CLASS_MAP: Record<string, MediaDeviceKind> = {
|
|
19
|
+
'Audio/Source': 'audioinput',
|
|
20
|
+
'Video/Source': 'videoinput',
|
|
21
|
+
'Audio/Sink': 'audiooutput',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Whether getUserMedia has been successfully called (unlocks full device info). */
|
|
25
|
+
let _permissionGranted = false;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if GStreamer device monitoring is safe to use.
|
|
29
|
+
* On some GJS/GStreamer combinations (e.g. Fedora 44 / GJS 1.88 in Docker),
|
|
30
|
+
* DeviceMonitor and DeviceProviderFactory can SIGSEGV in native code — a crash
|
|
31
|
+
* that JS error handling cannot intercept. We skip device monitoring entirely
|
|
32
|
+
* when DISPLAY is absent (headless/CI) since there are typically no audio/video
|
|
33
|
+
* devices in containers anyway.
|
|
34
|
+
*/
|
|
35
|
+
function isDeviceMonitorSafe(): boolean {
|
|
36
|
+
try {
|
|
37
|
+
// Import GLib to check environment — avoid crashing GStreamer APIs
|
|
38
|
+
const GLib = imports.gi.GLib;
|
|
39
|
+
// Skip in CI environments or headless containers
|
|
40
|
+
if (GLib.getenv('CI')) return false;
|
|
41
|
+
// No display = likely a container without devices
|
|
42
|
+
if (!GLib.getenv('DISPLAY') && !GLib.getenv('WAYLAND_DISPLAY')) return false;
|
|
43
|
+
return true;
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class MediaDevices extends EventTarget {
|
|
50
|
+
private _ondevicechange: ((ev: Event) => void) | null = null;
|
|
51
|
+
|
|
52
|
+
get ondevicechange(): ((ev: Event) => void) | null { return this._ondevicechange; }
|
|
53
|
+
set ondevicechange(v: ((ev: Event) => void) | null) { this._ondevicechange = v; }
|
|
54
|
+
|
|
55
|
+
async getUserMedia(constraints?: MediaStreamConstraints): Promise<MediaStream> {
|
|
56
|
+
if (!constraints) {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
"Failed to execute 'getUserMedia' on 'MediaDevices': At least one of audio or video must be requested",
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const stream = await getUserMedia(constraints);
|
|
62
|
+
_permissionGranted = true;
|
|
63
|
+
return stream;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async enumerateDevices(): Promise<MediaDeviceInfo[]> {
|
|
67
|
+
ensureGstInit();
|
|
68
|
+
|
|
69
|
+
let monitor: InstanceType<typeof Gst.DeviceMonitor> | null = null;
|
|
70
|
+
const result: MediaDeviceInfo[] = [];
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Guard: on CI containers without PipeWire/PulseAudio, DeviceMonitor
|
|
74
|
+
// can SIGSEGV in native GStreamer code. Check for device providers first.
|
|
75
|
+
if (!isDeviceMonitorSafe()) {
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
monitor = new Gst.DeviceMonitor();
|
|
80
|
+
monitor.set_show_all_devices(true);
|
|
81
|
+
const audioCaps = Gst.Caps.from_string('audio/x-raw');
|
|
82
|
+
const videoCaps = Gst.Caps.from_string('video/x-raw');
|
|
83
|
+
if (audioCaps) {
|
|
84
|
+
monitor.add_filter('Audio/Source', audioCaps);
|
|
85
|
+
monitor.add_filter('Audio/Sink', audioCaps);
|
|
86
|
+
}
|
|
87
|
+
if (videoCaps) {
|
|
88
|
+
monitor.add_filter('Video/Source', videoCaps);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!monitor.start()) {
|
|
92
|
+
// DeviceMonitor failed to start — return empty list gracefully
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
let gstDevices: any[];
|
|
97
|
+
try {
|
|
98
|
+
gstDevices = monitor.get_devices() ?? [];
|
|
99
|
+
} catch {
|
|
100
|
+
// get_devices() can crash on some GStreamer/GJS versions — return empty
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const device of gstDevices) {
|
|
105
|
+
const deviceClass = device.get_device_class?.() ?? '';
|
|
106
|
+
const kind = DEVICE_CLASS_MAP[deviceClass];
|
|
107
|
+
if (!kind) continue;
|
|
108
|
+
|
|
109
|
+
const displayName = device.get_display_name?.() ?? '';
|
|
110
|
+
let deviceId = '';
|
|
111
|
+
let groupId = '';
|
|
112
|
+
|
|
113
|
+
// Extract persistent-id from device properties if available
|
|
114
|
+
try {
|
|
115
|
+
const props = device.get_properties?.();
|
|
116
|
+
if (props) {
|
|
117
|
+
const n = props.n_fields();
|
|
118
|
+
for (let i = 0; i < n; i++) {
|
|
119
|
+
const name = props.nth_field_name(i);
|
|
120
|
+
if (name === 'persistent-id' || name === 'node.name') {
|
|
121
|
+
const val = props.get_value(name);
|
|
122
|
+
if (val && !deviceId) deviceId = String(val);
|
|
123
|
+
}
|
|
124
|
+
if (name === 'group-id') {
|
|
125
|
+
const val = props.get_value(name);
|
|
126
|
+
if (val) groupId = String(val);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} catch { /* properties may not be available */ }
|
|
131
|
+
|
|
132
|
+
// Fallback deviceId from display name hash
|
|
133
|
+
if (!deviceId) {
|
|
134
|
+
deviceId = displayName || `${kind}-${result.length}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Per W3C: before getUserMedia permission, expose only empty
|
|
138
|
+
// deviceId/label/groupId (one device per kind max).
|
|
139
|
+
if (_permissionGranted) {
|
|
140
|
+
result.push(new MediaDeviceInfo({
|
|
141
|
+
deviceId,
|
|
142
|
+
kind,
|
|
143
|
+
label: displayName,
|
|
144
|
+
groupId,
|
|
145
|
+
}));
|
|
146
|
+
} else {
|
|
147
|
+
// Check if we already have a device of this kind
|
|
148
|
+
if (!result.some(d => d.kind === kind)) {
|
|
149
|
+
result.push(new MediaDeviceInfo({
|
|
150
|
+
deviceId: '',
|
|
151
|
+
kind,
|
|
152
|
+
label: '',
|
|
153
|
+
groupId: '',
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// DeviceMonitor or device enumeration crashed — return whatever we have
|
|
160
|
+
return result;
|
|
161
|
+
} finally {
|
|
162
|
+
try { monitor?.stop(); } catch { /* ignore stop errors */ }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// W3C ordering: audioinput first, then videoinput, then audiooutput
|
|
166
|
+
const order: Record<string, number> = { audioinput: 0, videoinput: 1, audiooutput: 2 };
|
|
167
|
+
result.sort((a, b) => (order[a.kind] ?? 3) - (order[b.kind] ?? 3));
|
|
168
|
+
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
getSupportedConstraints(): Record<string, boolean> {
|
|
173
|
+
return {
|
|
174
|
+
deviceId: true,
|
|
175
|
+
width: true,
|
|
176
|
+
height: true,
|
|
177
|
+
frameRate: true,
|
|
178
|
+
sampleRate: true,
|
|
179
|
+
channelCount: true,
|
|
180
|
+
// Not yet supported — return false
|
|
181
|
+
aspectRatio: false,
|
|
182
|
+
facingMode: false,
|
|
183
|
+
resizeMode: false,
|
|
184
|
+
echoCancellation: false,
|
|
185
|
+
autoGainControl: false,
|
|
186
|
+
noiseSuppression: false,
|
|
187
|
+
latency: false,
|
|
188
|
+
groupId: false,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// W3C MediaStreamTrack for GJS.
|
|
2
|
+
//
|
|
3
|
+
// Phase 2: lightweight API surface with event dispatch.
|
|
4
|
+
// Phase 3: optional GStreamer source integration — tracks created by
|
|
5
|
+
// getUserMedia carry a GStreamer source element reference that the
|
|
6
|
+
// RTCRtpSender wires into the webrtcbin pipeline.
|
|
7
|
+
//
|
|
8
|
+
// Reference: refs/node-gst-webrtc/src/media/MediaStreamTrack.ts (ISC)
|
|
9
|
+
// Reference: W3C MediaStreamTrack spec
|
|
10
|
+
|
|
11
|
+
import '@gjsify/dom-events/register/event-target';
|
|
12
|
+
|
|
13
|
+
import GLib from 'gi://GLib?version=2.0';
|
|
14
|
+
|
|
15
|
+
import { Gst } from './gst-init.js';
|
|
16
|
+
|
|
17
|
+
/** @internal GStreamer backing for tracks created by getUserMedia */
|
|
18
|
+
export interface MediaStreamTrackGstInit {
|
|
19
|
+
source: any; // Gst.Element
|
|
20
|
+
pipeline: any; // Gst.Pipeline
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MediaStreamTrackInit {
|
|
24
|
+
kind: 'audio' | 'video';
|
|
25
|
+
label?: string;
|
|
26
|
+
id?: string;
|
|
27
|
+
muted?: boolean;
|
|
28
|
+
/** @internal */
|
|
29
|
+
_gst?: MediaStreamTrackGstInit;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class MediaStreamTrack extends EventTarget {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly kind: 'audio' | 'video';
|
|
35
|
+
readonly label: string;
|
|
36
|
+
|
|
37
|
+
private _enabled = true;
|
|
38
|
+
private _muted: boolean;
|
|
39
|
+
private _ended = false;
|
|
40
|
+
private _contentHint = '';
|
|
41
|
+
|
|
42
|
+
private _onended: ((ev: Event) => void) | null = null;
|
|
43
|
+
private _onmute: ((ev: Event) => void) | null = null;
|
|
44
|
+
private _onunmute: ((ev: Event) => void) | null = null;
|
|
45
|
+
|
|
46
|
+
/** @internal GStreamer source element (e.g. pulsesrc, audiotestsrc) */
|
|
47
|
+
_gstSource: any = null;
|
|
48
|
+
/** @internal Pipeline the source currently lives in (updated by VideoBridge) */
|
|
49
|
+
_gstPipeline: any = null;
|
|
50
|
+
/** @internal Tee element inserted by VideoBridge for preview fan-out */
|
|
51
|
+
_gstTee: any = null;
|
|
52
|
+
/** @internal TeeMultiplexer for multi-PC fan-out (created on second addTrack) */
|
|
53
|
+
_teeMultiplexer: any = null;
|
|
54
|
+
/** @internal Callback set by RTCRtpSender to control valve drop property */
|
|
55
|
+
private _enableCallback: ((enabled: boolean) => void) | null = null;
|
|
56
|
+
|
|
57
|
+
constructor(init: MediaStreamTrackInit) {
|
|
58
|
+
super();
|
|
59
|
+
this.id = init.id ?? GLib.uuid_string_random();
|
|
60
|
+
this.kind = init.kind;
|
|
61
|
+
this.label = init.label ?? '';
|
|
62
|
+
this._muted = init.muted ?? false;
|
|
63
|
+
|
|
64
|
+
if (init._gst) {
|
|
65
|
+
this._gstSource = init._gst.source;
|
|
66
|
+
this._gstPipeline = init._gst.pipeline;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
get enabled(): boolean { return this._enabled; }
|
|
71
|
+
set enabled(v: boolean) {
|
|
72
|
+
const val = !!v;
|
|
73
|
+
if (this._enabled === val) return;
|
|
74
|
+
this._enabled = val;
|
|
75
|
+
this._enableCallback?.(val);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
get muted(): boolean { return this._muted; }
|
|
79
|
+
|
|
80
|
+
get readyState(): 'live' | 'ended' { return this._ended ? 'ended' : 'live'; }
|
|
81
|
+
|
|
82
|
+
get contentHint(): string { return this._contentHint; }
|
|
83
|
+
set contentHint(v: string) {
|
|
84
|
+
if (this.kind === 'audio') {
|
|
85
|
+
if (v !== '' && v !== 'speech' && v !== 'speech-recognition' && v !== 'music') return;
|
|
86
|
+
} else {
|
|
87
|
+
if (v !== '' && v !== 'motion' && v !== 'detail' && v !== 'text') return;
|
|
88
|
+
}
|
|
89
|
+
this._contentHint = v;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get onended(): ((ev: Event) => void) | null { return this._onended; }
|
|
93
|
+
set onended(v: ((ev: Event) => void) | null) { this._onended = v; }
|
|
94
|
+
get onmute(): ((ev: Event) => void) | null { return this._onmute; }
|
|
95
|
+
set onmute(v: ((ev: Event) => void) | null) { this._onmute = v; }
|
|
96
|
+
get onunmute(): ((ev: Event) => void) | null { return this._onunmute; }
|
|
97
|
+
set onunmute(v: ((ev: Event) => void) | null) { this._onunmute = v; }
|
|
98
|
+
|
|
99
|
+
clone(): MediaStreamTrack {
|
|
100
|
+
const cloned = new MediaStreamTrack({
|
|
101
|
+
kind: this.kind,
|
|
102
|
+
label: this.label,
|
|
103
|
+
muted: this._muted,
|
|
104
|
+
});
|
|
105
|
+
cloned._enabled = this._enabled;
|
|
106
|
+
return cloned;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
stop(): void {
|
|
110
|
+
if (this._ended) return;
|
|
111
|
+
this._ended = true;
|
|
112
|
+
|
|
113
|
+
// Clean up GStreamer source and pipeline if present
|
|
114
|
+
if (this._gstSource || this._gstPipeline) {
|
|
115
|
+
try {
|
|
116
|
+
// Set pipeline to NULL first (this stops all children)
|
|
117
|
+
this._gstPipeline?.set_state(Gst.State.NULL);
|
|
118
|
+
} catch { /* ignore */ }
|
|
119
|
+
try {
|
|
120
|
+
this._gstSource?.set_state(Gst.State.NULL);
|
|
121
|
+
} catch { /* ignore */ }
|
|
122
|
+
this._gstSource = null;
|
|
123
|
+
this._gstPipeline = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const ev = new Event('ended');
|
|
127
|
+
this._onended?.call(this, ev);
|
|
128
|
+
this.dispatchEvent(ev);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
getCapabilities(): Record<string, unknown> { return {}; }
|
|
132
|
+
getConstraints(): Record<string, unknown> { return {}; }
|
|
133
|
+
getSettings(): Record<string, unknown> { return {}; }
|
|
134
|
+
|
|
135
|
+
applyConstraints(_constraints?: unknown): Promise<void> {
|
|
136
|
+
return Promise.reject(new DOMException(
|
|
137
|
+
'applyConstraints is not supported',
|
|
138
|
+
'NotSupportedError',
|
|
139
|
+
));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @internal — used by RTCRtpReceiver to toggle mute state */
|
|
143
|
+
_setMuted(muted: boolean): void {
|
|
144
|
+
if (this._muted === muted) return;
|
|
145
|
+
this._muted = muted;
|
|
146
|
+
const ev = new Event(muted ? 'mute' : 'unmute');
|
|
147
|
+
if (muted) {
|
|
148
|
+
this._onmute?.call(this, ev);
|
|
149
|
+
} else {
|
|
150
|
+
this._onunmute?.call(this, ev);
|
|
151
|
+
}
|
|
152
|
+
this.dispatchEvent(ev);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** @internal — called by RTCRtpSender to wire valve control */
|
|
156
|
+
_setEnableCallback(cb: ((enabled: boolean) => void) | null): void {
|
|
157
|
+
this._enableCallback = cb;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// W3C MediaStream for GJS.
|
|
2
|
+
//
|
|
3
|
+
// Pure-JS collection container for MediaStreamTrack instances.
|
|
4
|
+
// No GStreamer pipeline integration — that is Phase 2.5.
|
|
5
|
+
//
|
|
6
|
+
// Reference: refs/node-gst-webrtc/src/media/MediaStream.ts (ISC)
|
|
7
|
+
// Reference: W3C MediaStream spec
|
|
8
|
+
|
|
9
|
+
import '@gjsify/dom-events/register/event-target';
|
|
10
|
+
|
|
11
|
+
import GLib from 'gi://GLib?version=2.0';
|
|
12
|
+
|
|
13
|
+
import { MediaStreamTrack } from './media-stream-track.js';
|
|
14
|
+
|
|
15
|
+
export class MediaStream extends EventTarget {
|
|
16
|
+
readonly id: string;
|
|
17
|
+
private _tracks = new Map<string, MediaStreamTrack>();
|
|
18
|
+
|
|
19
|
+
private _onaddtrack: ((ev: Event) => void) | null = null;
|
|
20
|
+
private _onremovetrack: ((ev: Event) => void) | null = null;
|
|
21
|
+
|
|
22
|
+
constructor(streamOrTracks?: MediaStream | MediaStreamTrack[]) {
|
|
23
|
+
super();
|
|
24
|
+
this.id = GLib.uuid_string_random();
|
|
25
|
+
|
|
26
|
+
if (streamOrTracks instanceof MediaStream) {
|
|
27
|
+
for (const track of streamOrTracks.getTracks()) {
|
|
28
|
+
this._tracks.set(track.id, track.clone());
|
|
29
|
+
}
|
|
30
|
+
} else if (Array.isArray(streamOrTracks)) {
|
|
31
|
+
for (const track of streamOrTracks) {
|
|
32
|
+
this._tracks.set(track.id, track);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get active(): boolean {
|
|
38
|
+
for (const track of this._tracks.values()) {
|
|
39
|
+
if (track.readyState === 'live') return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get onaddtrack(): ((ev: Event) => void) | null { return this._onaddtrack; }
|
|
45
|
+
set onaddtrack(v: ((ev: Event) => void) | null) { this._onaddtrack = v; }
|
|
46
|
+
get onremovetrack(): ((ev: Event) => void) | null { return this._onremovetrack; }
|
|
47
|
+
set onremovetrack(v: ((ev: Event) => void) | null) { this._onremovetrack = v; }
|
|
48
|
+
|
|
49
|
+
getTracks(): MediaStreamTrack[] {
|
|
50
|
+
return [...this._tracks.values()];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getAudioTracks(): MediaStreamTrack[] {
|
|
54
|
+
return this.getTracks().filter((t) => t.kind === 'audio');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getVideoTracks(): MediaStreamTrack[] {
|
|
58
|
+
return this.getTracks().filter((t) => t.kind === 'video');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getTrackById(id: string): MediaStreamTrack | null {
|
|
62
|
+
return this._tracks.get(id) ?? null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
addTrack(track: MediaStreamTrack): void {
|
|
66
|
+
if (this._tracks.has(track.id)) return;
|
|
67
|
+
this._tracks.set(track.id, track);
|
|
68
|
+
const ev = new MediaStreamTrackEvent('addtrack', { track });
|
|
69
|
+
this._onaddtrack?.call(this, ev);
|
|
70
|
+
this.dispatchEvent(ev);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
removeTrack(track: MediaStreamTrack): void {
|
|
74
|
+
if (!this._tracks.delete(track.id)) return;
|
|
75
|
+
const ev = new MediaStreamTrackEvent('removetrack', { track });
|
|
76
|
+
this._onremovetrack?.call(this, ev);
|
|
77
|
+
this.dispatchEvent(ev);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
clone(): MediaStream {
|
|
81
|
+
return new MediaStream(this);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface MediaStreamTrackEventInit extends EventInit {
|
|
86
|
+
track: MediaStreamTrack;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class MediaStreamTrackEvent extends Event {
|
|
90
|
+
readonly track: MediaStreamTrack;
|
|
91
|
+
|
|
92
|
+
constructor(type: string, init: MediaStreamTrackEventInit) {
|
|
93
|
+
super(type, init);
|
|
94
|
+
this.track = init.track;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Registers: RTCDataChannel, RTCDataChannelEvent.
|
|
2
|
+
|
|
3
|
+
import { RTCDataChannel } from '../rtc-data-channel.js';
|
|
4
|
+
import { RTCDataChannelEvent } from '../rtc-events.js';
|
|
5
|
+
|
|
6
|
+
if (typeof (globalThis as any).RTCDataChannel === 'undefined') {
|
|
7
|
+
(globalThis as any).RTCDataChannel = RTCDataChannel;
|
|
8
|
+
}
|
|
9
|
+
if (typeof (globalThis as any).RTCDataChannelEvent === 'undefined') {
|
|
10
|
+
(globalThis as any).RTCDataChannelEvent = RTCDataChannelEvent;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Registers: RTCError, RTCErrorEvent.
|
|
2
|
+
|
|
3
|
+
import { RTCError } from '../rtc-error.js';
|
|
4
|
+
import { RTCErrorEvent } from '../rtc-events.js';
|
|
5
|
+
|
|
6
|
+
if (typeof (globalThis as any).RTCError === 'undefined') {
|
|
7
|
+
(globalThis as any).RTCError = RTCError;
|
|
8
|
+
}
|
|
9
|
+
if (typeof (globalThis as any).RTCErrorEvent === 'undefined') {
|
|
10
|
+
(globalThis as any).RTCErrorEvent = RTCErrorEvent;
|
|
11
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Register navigator.mediaDevices on globalThis for GJS.
|
|
2
|
+
|
|
3
|
+
import { MediaDevices } from '../media-devices.js';
|
|
4
|
+
|
|
5
|
+
if (typeof (globalThis as any).navigator === 'undefined') {
|
|
6
|
+
(globalThis as any).navigator = {} as any;
|
|
7
|
+
}
|
|
8
|
+
if (typeof (globalThis as any).navigator.mediaDevices === 'undefined') {
|
|
9
|
+
(globalThis as any).navigator.mediaDevices = new MediaDevices();
|
|
10
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Registers: MediaStream, MediaStreamTrack, RTCTrackEvent.
|
|
2
|
+
|
|
3
|
+
import { MediaStream } from '../media-stream.js';
|
|
4
|
+
import { MediaStreamTrack } from '../media-stream-track.js';
|
|
5
|
+
import { RTCTrackEvent } from '../rtc-track-event.js';
|
|
6
|
+
|
|
7
|
+
if (typeof (globalThis as any).MediaStream === 'undefined') {
|
|
8
|
+
(globalThis as any).MediaStream = MediaStream;
|
|
9
|
+
}
|
|
10
|
+
if (typeof (globalThis as any).MediaStreamTrack === 'undefined') {
|
|
11
|
+
(globalThis as any).MediaStreamTrack = MediaStreamTrack;
|
|
12
|
+
}
|
|
13
|
+
if (typeof (globalThis as any).RTCTrackEvent === 'undefined') {
|
|
14
|
+
(globalThis as any).RTCTrackEvent = RTCTrackEvent;
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Registers: RTCPeerConnection, RTCSessionDescription, RTCIceCandidate,
|
|
2
|
+
// RTCPeerConnectionIceEvent.
|
|
3
|
+
|
|
4
|
+
import { RTCPeerConnection } from '../rtc-peer-connection.js';
|
|
5
|
+
import { RTCSessionDescription } from '../rtc-session-description.js';
|
|
6
|
+
import { RTCIceCandidate } from '../rtc-ice-candidate.js';
|
|
7
|
+
import { RTCPeerConnectionIceEvent } from '../rtc-events.js';
|
|
8
|
+
|
|
9
|
+
if (typeof (globalThis as any).RTCPeerConnection === 'undefined') {
|
|
10
|
+
(globalThis as any).RTCPeerConnection = RTCPeerConnection;
|
|
11
|
+
}
|
|
12
|
+
if (typeof (globalThis as any).RTCSessionDescription === 'undefined') {
|
|
13
|
+
(globalThis as any).RTCSessionDescription = RTCSessionDescription;
|
|
14
|
+
}
|
|
15
|
+
if (typeof (globalThis as any).RTCIceCandidate === 'undefined') {
|
|
16
|
+
(globalThis as any).RTCIceCandidate = RTCIceCandidate;
|
|
17
|
+
}
|
|
18
|
+
if (typeof (globalThis as any).RTCPeerConnectionIceEvent === 'undefined') {
|
|
19
|
+
(globalThis as any).RTCPeerConnectionIceEvent = RTCPeerConnectionIceEvent;
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// WebRTC globals registration tests — verifies the granular /register
|
|
2
|
+
// subpaths wire each identifier onto globalThis.
|
|
3
|
+
//
|
|
4
|
+
// These tests must run AFTER importing the register subpaths — in `test.mts`
|
|
5
|
+
// the register module is imported alongside the spec, so the assertions below
|
|
6
|
+
// run post-registration.
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from '@gjsify/unit';
|
|
9
|
+
|
|
10
|
+
import '@gjsify/webrtc/register';
|
|
11
|
+
|
|
12
|
+
export default async () => {
|
|
13
|
+
await describe('@gjsify/webrtc/register', async () => {
|
|
14
|
+
await it('registers RTCPeerConnection', async () => {
|
|
15
|
+
expect(typeof (globalThis as any).RTCPeerConnection).toBe('function');
|
|
16
|
+
});
|
|
17
|
+
await it('registers RTCSessionDescription', async () => {
|
|
18
|
+
expect(typeof (globalThis as any).RTCSessionDescription).toBe('function');
|
|
19
|
+
});
|
|
20
|
+
await it('registers RTCIceCandidate', async () => {
|
|
21
|
+
expect(typeof (globalThis as any).RTCIceCandidate).toBe('function');
|
|
22
|
+
});
|
|
23
|
+
await it('registers RTCPeerConnectionIceEvent', async () => {
|
|
24
|
+
expect(typeof (globalThis as any).RTCPeerConnectionIceEvent).toBe('function');
|
|
25
|
+
});
|
|
26
|
+
await it('registers RTCDataChannel', async () => {
|
|
27
|
+
expect(typeof (globalThis as any).RTCDataChannel).toBe('function');
|
|
28
|
+
});
|
|
29
|
+
await it('registers RTCDataChannelEvent', async () => {
|
|
30
|
+
expect(typeof (globalThis as any).RTCDataChannelEvent).toBe('function');
|
|
31
|
+
});
|
|
32
|
+
await it('registers RTCError', async () => {
|
|
33
|
+
expect(typeof (globalThis as any).RTCError).toBe('function');
|
|
34
|
+
});
|
|
35
|
+
await it('registers RTCErrorEvent', async () => {
|
|
36
|
+
expect(typeof (globalThis as any).RTCErrorEvent).toBe('function');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await it('RTCError extends DOMException', async () => {
|
|
40
|
+
const err = new (globalThis as any).RTCError({ errorDetail: 'data-channel-failure' }, 'test');
|
|
41
|
+
const DOMExceptionCtor = (globalThis as any).DOMException;
|
|
42
|
+
if (DOMExceptionCtor) {
|
|
43
|
+
expect(err instanceof DOMExceptionCtor).toBeTruthy();
|
|
44
|
+
}
|
|
45
|
+
expect(err.errorDetail).toBe('data-channel-failure');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await it('RTCPeerConnectionIceEvent is a subclass of Event', async () => {
|
|
49
|
+
const RTCPeerConnectionIceEventCtor = (globalThis as any).RTCPeerConnectionIceEvent;
|
|
50
|
+
const ev = new RTCPeerConnectionIceEventCtor('icecandidate', { candidate: null });
|
|
51
|
+
expect(ev instanceof Event).toBeTruthy();
|
|
52
|
+
expect(ev.candidate).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
};
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Catch-all side-effect module: registers all WebRTC globals on GJS.
|
|
2
|
+
// Prefer granular imports (e.g. '@gjsify/webrtc/register/data-channel')
|
|
3
|
+
// when only specific globals are needed — the --globals auto mode does this
|
|
4
|
+
// automatically.
|
|
5
|
+
|
|
6
|
+
import './register/peer-connection.js';
|
|
7
|
+
import './register/data-channel.js';
|
|
8
|
+
import './register/error.js';
|
|
9
|
+
import './register/media.js';
|
|
10
|
+
import './register/media-devices.js';
|