@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.
Files changed (44) hide show
  1. package/package.json +73 -70
  2. package/src/get-user-media.ts +0 -131
  3. package/src/gst-enum-maps.ts +0 -125
  4. package/src/gst-init.ts +0 -49
  5. package/src/gst-stats-parser.ts +0 -137
  6. package/src/gst-utils.ts +0 -41
  7. package/src/index.ts +0 -104
  8. package/src/internal/gst-types.ts +0 -122
  9. package/src/media-device-info.ts +0 -33
  10. package/src/media-devices.ts +0 -191
  11. package/src/media-stream-track.ts +0 -159
  12. package/src/media-stream.ts +0 -96
  13. package/src/register/data-channel.ts +0 -11
  14. package/src/register/error.ts +0 -11
  15. package/src/register/media-devices.ts +0 -10
  16. package/src/register/media.ts +0 -15
  17. package/src/register/peer-connection.ts +0 -20
  18. package/src/register.spec.ts +0 -55
  19. package/src/register.ts +0 -10
  20. package/src/rtc-certificate.ts +0 -110
  21. package/src/rtc-data-channel.ts +0 -283
  22. package/src/rtc-dtls-transport.ts +0 -48
  23. package/src/rtc-dtmf-sender.ts +0 -146
  24. package/src/rtc-error.ts +0 -49
  25. package/src/rtc-events.ts +0 -64
  26. package/src/rtc-ice-candidate.ts +0 -115
  27. package/src/rtc-ice-transport.ts +0 -104
  28. package/src/rtc-peer-connection.ts +0 -1039
  29. package/src/rtc-rtp-receiver.ts +0 -122
  30. package/src/rtc-rtp-sender.ts +0 -471
  31. package/src/rtc-rtp-transceiver.ts +0 -131
  32. package/src/rtc-sctp-transport.ts +0 -48
  33. package/src/rtc-session-description.ts +0 -64
  34. package/src/rtc-stats-report.ts +0 -39
  35. package/src/rtc-track-event.ts +0 -45
  36. package/src/rtp-capabilities.ts +0 -48
  37. package/src/tee-multiplexer.ts +0 -75
  38. package/src/test.mts +0 -11
  39. package/src/webrtc.spec.ts +0 -1186
  40. package/src/wpt-helpers.ts +0 -156
  41. package/src/wpt-media.spec.ts +0 -1154
  42. package/src/wpt.spec.ts +0 -1136
  43. package/tsconfig.json +0 -36
  44. package/tsconfig.tsbuildinfo +0 -1
package/package.json CHANGED
@@ -1,75 +1,78 @@
1
1
  {
2
- "name": "@gjsify/webrtc",
3
- "version": "0.4.0",
4
- "description": "W3C WebRTC API for GJS using GStreamer webrtcbin as the peer-connection backend",
5
- "type": "module",
6
- "module": "lib/esm/index.js",
7
- "types": "lib/types/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./lib/types/index.d.ts",
11
- "default": "./lib/esm/index.js"
2
+ "name": "@gjsify/webrtc",
3
+ "version": "0.4.3",
4
+ "description": "W3C WebRTC API for GJS using GStreamer webrtcbin as the peer-connection backend",
5
+ "type": "module",
6
+ "module": "lib/esm/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/esm/index.js"
12
+ },
13
+ "./register": {
14
+ "types": "./lib/types/register.d.ts",
15
+ "default": "./lib/esm/register.js"
16
+ },
17
+ "./register/peer-connection": {
18
+ "default": "./lib/esm/register/peer-connection.js"
19
+ },
20
+ "./register/data-channel": {
21
+ "default": "./lib/esm/register/data-channel.js"
22
+ },
23
+ "./register/error": {
24
+ "default": "./lib/esm/register/error.js"
25
+ },
26
+ "./register/media": {
27
+ "default": "./lib/esm/register/media.js"
28
+ },
29
+ "./register/media-devices": {
30
+ "default": "./lib/esm/register/media-devices.js"
31
+ }
12
32
  },
13
- "./register": {
14
- "types": "./lib/types/register.d.ts",
15
- "default": "./lib/esm/register.js"
33
+ "files": [
34
+ "lib"
35
+ ],
36
+ "sideEffects": [
37
+ "./lib/esm/register.js",
38
+ "./lib/esm/register/*.js"
39
+ ],
40
+ "scripts": {
41
+ "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs || exit 0",
42
+ "check": "tsc --noEmit",
43
+ "build": "gjsify run build:gjsify && gjsify run build:types",
44
+ "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
45
+ "build:types": "tsc",
46
+ "build:test": "gjsify run build:test:gjs",
47
+ "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
48
+ "test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:gjs",
49
+ "test:gjs": "gjsify run test.gjs.mjs"
16
50
  },
17
- "./register/peer-connection": {
18
- "default": "./lib/esm/register/peer-connection.js"
51
+ "keywords": [
52
+ "gjs",
53
+ "webrtc",
54
+ "gstreamer",
55
+ "webrtcbin",
56
+ "rtc",
57
+ "data-channel",
58
+ "peer-connection"
59
+ ],
60
+ "dependencies": {
61
+ "@gjsify/buffer": "workspace:^",
62
+ "@gjsify/dom-events": "workspace:^",
63
+ "@gjsify/dom-exception": "workspace:^",
64
+ "@gjsify/webrtc-native": "workspace:^"
19
65
  },
20
- "./register/data-channel": {
21
- "default": "./lib/esm/register/data-channel.js"
22
- },
23
- "./register/error": {
24
- "default": "./lib/esm/register/error.js"
25
- },
26
- "./register/media": {
27
- "default": "./lib/esm/register/media.js"
28
- },
29
- "./register/media-devices": {
30
- "default": "./lib/esm/register/media-devices.js"
66
+ "devDependencies": {
67
+ "@girs/gjs": "4.0.0-rc.15",
68
+ "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
69
+ "@girs/gobject-2.0": "2.88.0-4.0.0-rc.15",
70
+ "@girs/gst-1.0": "1.28.1-4.0.0-rc.15",
71
+ "@girs/gstsdp-1.0": "1.0.0-4.0.0-rc.15",
72
+ "@girs/gstwebrtc-1.0": "1.0.0-4.0.0-rc.15",
73
+ "@gjsify/cli": "workspace:^",
74
+ "@gjsify/unit": "workspace:^",
75
+ "@types/node": "^25.6.2",
76
+ "typescript": "^6.0.3"
31
77
  }
32
- },
33
- "sideEffects": [
34
- "./lib/esm/register.js",
35
- "./lib/esm/register/*.js"
36
- ],
37
- "scripts": {
38
- "clear": "rm -rf lib tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo test.gjs.mjs || exit 0",
39
- "check": "tsc --noEmit",
40
- "build": "yarn build:gjsify && yarn build:types",
41
- "build:gjsify": "gjsify build --library 'src/**/*.{ts,js}' --exclude 'src/**/*.spec.{mts,ts}' 'src/test.{mts,ts}'",
42
- "build:types": "tsc",
43
- "build:test": "yarn build:test:gjs",
44
- "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
45
- "test": "yarn build:gjsify && yarn build:test && yarn test:gjs",
46
- "test:gjs": "gjsify run test.gjs.mjs"
47
- },
48
- "keywords": [
49
- "gjs",
50
- "webrtc",
51
- "gstreamer",
52
- "webrtcbin",
53
- "rtc",
54
- "data-channel",
55
- "peer-connection"
56
- ],
57
- "dependencies": {
58
- "@gjsify/buffer": "^0.4.0",
59
- "@gjsify/dom-events": "^0.4.0",
60
- "@gjsify/dom-exception": "^0.4.0",
61
- "@gjsify/webrtc-native": "^0.4.0"
62
- },
63
- "devDependencies": {
64
- "@girs/gjs": "4.0.0-rc.15",
65
- "@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
66
- "@girs/gobject-2.0": "2.88.0-4.0.0-rc.15",
67
- "@girs/gst-1.0": "1.28.1-4.0.0-rc.15",
68
- "@girs/gstsdp-1.0": "1.0.0-4.0.0-rc.15",
69
- "@girs/gstwebrtc-1.0": "1.0.0-4.0.0-rc.15",
70
- "@gjsify/cli": "^0.4.0",
71
- "@gjsify/unit": "^0.4.0",
72
- "@types/node": "^25.6.2",
73
- "typescript": "^6.0.3"
74
- }
75
- }
78
+ }
@@ -1,131 +0,0 @@
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
- }
@@ -1,125 +0,0 @@
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): GstWebRTC.WebRTCRTPTransceiverDirection {
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 DELETED
@@ -1,49 +0,0 @@
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
- import { DOMException } from '@gjsify/dom-exception';
11
-
12
- let initialized = false;
13
-
14
- export function ensureGstInit(): void {
15
- if (initialized) return;
16
- Gst.init(null);
17
- initialized = true;
18
- }
19
-
20
- /** Throws if the `webrtcbin` element is not registered (gst-plugins-bad missing). */
21
- export function ensureWebrtcbinAvailable(): void {
22
- ensureGstInit();
23
- const webrtcFactory = Gst.ElementFactory.find('webrtcbin');
24
- if (!webrtcFactory) {
25
- throwNotSupported(
26
- 'GStreamer element "webrtcbin" not available. Install gst-plugins-bad:\n' +
27
- ' Fedora: dnf install gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-extras\n' +
28
- ' Ubuntu/Debian: apt install gstreamer1.0-plugins-bad',
29
- );
30
- }
31
- // webrtcbin requires libnice's GStreamer plugin for ICE transport —
32
- // without it, pipeline state-change to PLAYING fails and createDataChannel
33
- // hits the "webrtc->priv->is_closed" assertion.
34
- const niceFactory = Gst.ElementFactory.find('nicesrc');
35
- if (!niceFactory) {
36
- throwNotSupported(
37
- 'GStreamer "nice" plugin (libnice-gstreamer) not available — required by webrtcbin.\n' +
38
- ' Fedora: dnf install libnice-gstreamer1\n' +
39
- ' Ubuntu/Debian: apt install gstreamer1.0-nice\n' +
40
- ' Verify with: gst-inspect-1.0 nicesrc',
41
- );
42
- }
43
- }
44
-
45
- function throwNotSupported(message: string): never {
46
- throw new DOMException(message, 'NotSupportedError');
47
- }
48
-
49
- export { Gst };
@@ -1,137 +0,0 @@
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 DELETED
@@ -1,41 +0,0 @@
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
- }