@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.
Files changed (117) hide show
  1. package/lib/esm/get-user-media.js +93 -0
  2. package/lib/esm/gst-enum-maps.js +88 -0
  3. package/lib/esm/gst-init.js +34 -0
  4. package/lib/esm/gst-stats-parser.js +79 -0
  5. package/lib/esm/gst-utils.js +16 -0
  6. package/lib/esm/index.js +53 -0
  7. package/lib/esm/media-device-info.js +23 -0
  8. package/lib/esm/media-devices.js +147 -0
  9. package/lib/esm/media-stream-track.js +142 -0
  10. package/lib/esm/media-stream.js +78 -0
  11. package/lib/esm/register/data-channel.js +8 -0
  12. package/lib/esm/register/error.js +8 -0
  13. package/lib/esm/register/media-devices.js +7 -0
  14. package/lib/esm/register/media.js +12 -0
  15. package/lib/esm/register/peer-connection.js +16 -0
  16. package/lib/esm/register.js +5 -0
  17. package/lib/esm/rtc-certificate.js +70 -0
  18. package/lib/esm/rtc-data-channel.js +266 -0
  19. package/lib/esm/rtc-dtls-transport.js +41 -0
  20. package/lib/esm/rtc-dtmf-sender.js +109 -0
  21. package/lib/esm/rtc-error.js +24 -0
  22. package/lib/esm/rtc-events.js +35 -0
  23. package/lib/esm/rtc-ice-candidate.js +75 -0
  24. package/lib/esm/rtc-ice-transport.js +96 -0
  25. package/lib/esm/rtc-peer-connection.js +855 -0
  26. package/lib/esm/rtc-rtp-receiver.js +91 -0
  27. package/lib/esm/rtc-rtp-sender.js +298 -0
  28. package/lib/esm/rtc-rtp-transceiver.js +97 -0
  29. package/lib/esm/rtc-sctp-transport.js +40 -0
  30. package/lib/esm/rtc-session-description.js +57 -0
  31. package/lib/esm/rtc-stats-report.js +35 -0
  32. package/lib/esm/rtc-track-event.js +29 -0
  33. package/lib/esm/rtp-capabilities.js +41 -0
  34. package/lib/esm/tee-multiplexer.js +62 -0
  35. package/lib/esm/wpt-helpers.js +122 -0
  36. package/lib/types/get-user-media.d.ts +14 -0
  37. package/lib/types/gst-enum-maps.d.ts +10 -0
  38. package/lib/types/gst-init.d.ts +5 -0
  39. package/lib/types/gst-stats-parser.d.ts +16 -0
  40. package/lib/types/gst-utils.d.ts +11 -0
  41. package/lib/types/index.d.ts +41 -0
  42. package/lib/types/media-device-info.d.ts +14 -0
  43. package/lib/types/media-devices.d.ts +12 -0
  44. package/lib/types/media-stream-track.d.ts +59 -0
  45. package/lib/types/media-stream.d.ts +28 -0
  46. package/lib/types/register/data-channel.d.ts +1 -0
  47. package/lib/types/register/error.d.ts +1 -0
  48. package/lib/types/register/media-devices.d.ts +1 -0
  49. package/lib/types/register/media.d.ts +1 -0
  50. package/lib/types/register/peer-connection.d.ts +1 -0
  51. package/lib/types/register.d.ts +5 -0
  52. package/lib/types/register.spec.d.ts +3 -0
  53. package/lib/types/rtc-certificate.d.ts +23 -0
  54. package/lib/types/rtc-data-channel.d.ts +64 -0
  55. package/lib/types/rtc-dtls-transport.d.ts +20 -0
  56. package/lib/types/rtc-dtmf-sender.d.ts +31 -0
  57. package/lib/types/rtc-error.d.ts +19 -0
  58. package/lib/types/rtc-events.d.ts +27 -0
  59. package/lib/types/rtc-ice-candidate.d.ts +28 -0
  60. package/lib/types/rtc-ice-transport.d.ts +56 -0
  61. package/lib/types/rtc-peer-connection.d.ts +165 -0
  62. package/lib/types/rtc-rtp-receiver.d.ts +45 -0
  63. package/lib/types/rtc-rtp-sender.d.ts +98 -0
  64. package/lib/types/rtc-rtp-transceiver.d.ts +20 -0
  65. package/lib/types/rtc-sctp-transport.d.ts +20 -0
  66. package/lib/types/rtc-session-description.d.ts +18 -0
  67. package/lib/types/rtc-stats-report.d.ts +22 -0
  68. package/lib/types/rtc-track-event.d.ts +18 -0
  69. package/lib/types/rtp-capabilities.d.ts +3 -0
  70. package/lib/types/tee-multiplexer.d.ts +25 -0
  71. package/lib/types/webrtc.spec.d.ts +2 -0
  72. package/lib/types/wpt-helpers.d.ts +30 -0
  73. package/lib/types/wpt-media.spec.d.ts +2 -0
  74. package/lib/types/wpt.spec.d.ts +2 -0
  75. package/package.json +74 -0
  76. package/src/get-user-media.ts +131 -0
  77. package/src/gst-enum-maps.ts +125 -0
  78. package/src/gst-init.ts +52 -0
  79. package/src/gst-stats-parser.ts +137 -0
  80. package/src/gst-utils.ts +41 -0
  81. package/src/index.ts +104 -0
  82. package/src/media-device-info.ts +33 -0
  83. package/src/media-devices.ts +191 -0
  84. package/src/media-stream-track.ts +159 -0
  85. package/src/media-stream.ts +96 -0
  86. package/src/register/data-channel.ts +11 -0
  87. package/src/register/error.ts +11 -0
  88. package/src/register/media-devices.ts +10 -0
  89. package/src/register/media.ts +15 -0
  90. package/src/register/peer-connection.ts +20 -0
  91. package/src/register.spec.ts +55 -0
  92. package/src/register.ts +10 -0
  93. package/src/rtc-certificate.ts +110 -0
  94. package/src/rtc-data-channel.ts +284 -0
  95. package/src/rtc-dtls-transport.ts +48 -0
  96. package/src/rtc-dtmf-sender.ts +146 -0
  97. package/src/rtc-error.ts +49 -0
  98. package/src/rtc-events.ts +64 -0
  99. package/src/rtc-ice-candidate.ts +115 -0
  100. package/src/rtc-ice-transport.ts +104 -0
  101. package/src/rtc-peer-connection.ts +1017 -0
  102. package/src/rtc-rtp-receiver.ts +122 -0
  103. package/src/rtc-rtp-sender.ts +444 -0
  104. package/src/rtc-rtp-transceiver.ts +127 -0
  105. package/src/rtc-sctp-transport.ts +48 -0
  106. package/src/rtc-session-description.ts +64 -0
  107. package/src/rtc-stats-report.ts +39 -0
  108. package/src/rtc-track-event.ts +45 -0
  109. package/src/rtp-capabilities.ts +48 -0
  110. package/src/tee-multiplexer.ts +75 -0
  111. package/src/test.mts +11 -0
  112. package/src/webrtc.spec.ts +1186 -0
  113. package/src/wpt-helpers.ts +156 -0
  114. package/src/wpt-media.spec.ts +1154 -0
  115. package/src/wpt.spec.ts +1136 -0
  116. package/tsconfig.json +36 -0
  117. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,93 @@
1
+ import { Gst, ensureGstInit } from "./gst-init.js";
2
+ import { MediaStreamTrack } from "./media-stream-track.js";
3
+ import { MediaStream } from "./media-stream.js";
4
+ async function getUserMedia(constraints) {
5
+ ensureGstInit();
6
+ if (!constraints.audio && !constraints.video) {
7
+ throw new TypeError(
8
+ "Failed to execute 'getUserMedia': At least one of audio or video must be requested"
9
+ );
10
+ }
11
+ const tracks = [];
12
+ if (constraints.audio) {
13
+ const audioConstraints = typeof constraints.audio === "object" ? constraints.audio : {};
14
+ const source = _createAudioSource();
15
+ const pipeline = new Gst.Pipeline();
16
+ pipeline.add(source);
17
+ const capsStr = _buildAudioCaps(audioConstraints);
18
+ if (capsStr) {
19
+ const capsfilter = Gst.ElementFactory.make("capsfilter", null);
20
+ capsfilter.caps = Gst.Caps.from_string(capsStr);
21
+ pipeline.add(capsfilter);
22
+ source.link(capsfilter);
23
+ }
24
+ tracks.push(new MediaStreamTrack({
25
+ kind: "audio",
26
+ label: source.name ?? "audio",
27
+ _gst: { source, pipeline }
28
+ }));
29
+ }
30
+ if (constraints.video) {
31
+ const videoConstraints = typeof constraints.video === "object" ? constraints.video : {};
32
+ const source = _createVideoSource();
33
+ const pipeline = new Gst.Pipeline();
34
+ pipeline.add(source);
35
+ const capsStr = _buildVideoCaps(videoConstraints);
36
+ if (capsStr) {
37
+ const capsfilter = Gst.ElementFactory.make("capsfilter", null);
38
+ capsfilter.caps = Gst.Caps.from_string(capsStr);
39
+ pipeline.add(capsfilter);
40
+ source.link(capsfilter);
41
+ }
42
+ tracks.push(new MediaStreamTrack({
43
+ kind: "video",
44
+ label: source.name ?? "video",
45
+ _gst: { source, pipeline }
46
+ }));
47
+ }
48
+ return new MediaStream(tracks);
49
+ }
50
+ function _buildAudioCaps(c) {
51
+ const parts = [];
52
+ if (c.sampleRate != null) parts.push(`rate=${Math.trunc(c.sampleRate)}`);
53
+ if (c.channelCount != null) parts.push(`channels=${Math.trunc(c.channelCount)}`);
54
+ if (parts.length === 0) return null;
55
+ return `audio/x-raw,${parts.join(",")}`;
56
+ }
57
+ function _buildVideoCaps(c) {
58
+ const parts = [];
59
+ if (c.width != null) parts.push(`width=${Math.trunc(c.width)}`);
60
+ if (c.height != null) parts.push(`height=${Math.trunc(c.height)}`);
61
+ if (c.frameRate != null) parts.push(`framerate=${Math.trunc(c.frameRate)}/1`);
62
+ if (parts.length === 0) return null;
63
+ return `video/x-raw,${parts.join(",")}`;
64
+ }
65
+ function _createAudioSource() {
66
+ for (const name of ["pipewiresrc", "pulsesrc", "autoaudiosrc"]) {
67
+ const el2 = Gst.ElementFactory.make(name, null);
68
+ if (el2) {
69
+ try {
70
+ el2.is_live = true;
71
+ } catch {
72
+ }
73
+ return el2;
74
+ }
75
+ }
76
+ const el = Gst.ElementFactory.make("audiotestsrc", null);
77
+ el.is_live = true;
78
+ el.wave = 0;
79
+ return el;
80
+ }
81
+ function _createVideoSource() {
82
+ for (const name of ["pipewiresrc", "v4l2src", "autovideosrc"]) {
83
+ const el2 = Gst.ElementFactory.make(name, null);
84
+ if (el2) return el2;
85
+ }
86
+ const el = Gst.ElementFactory.make("videotestsrc", null);
87
+ el.is_live = true;
88
+ el.pattern = 0;
89
+ return el;
90
+ }
91
+ export {
92
+ getUserMedia
93
+ };
@@ -0,0 +1,88 @@
1
+ import GstWebRTC from "gi://GstWebRTC?version=1.0";
2
+ const SIGNALING_STATE_MAP = {
3
+ [GstWebRTC.WebRTCSignalingState.STABLE]: "stable",
4
+ [GstWebRTC.WebRTCSignalingState.CLOSED]: "closed",
5
+ [GstWebRTC.WebRTCSignalingState.HAVE_LOCAL_OFFER]: "have-local-offer",
6
+ [GstWebRTC.WebRTCSignalingState.HAVE_REMOTE_OFFER]: "have-remote-offer",
7
+ [GstWebRTC.WebRTCSignalingState.HAVE_LOCAL_PRANSWER]: "have-local-pranswer",
8
+ [GstWebRTC.WebRTCSignalingState.HAVE_REMOTE_PRANSWER]: "have-remote-pranswer"
9
+ };
10
+ function gstToSignalingState(v) {
11
+ return SIGNALING_STATE_MAP[v] ?? "stable";
12
+ }
13
+ const CONNECTION_STATE_MAP = {
14
+ [GstWebRTC.WebRTCPeerConnectionState.NEW]: "new",
15
+ [GstWebRTC.WebRTCPeerConnectionState.CONNECTING]: "connecting",
16
+ [GstWebRTC.WebRTCPeerConnectionState.CONNECTED]: "connected",
17
+ [GstWebRTC.WebRTCPeerConnectionState.DISCONNECTED]: "disconnected",
18
+ [GstWebRTC.WebRTCPeerConnectionState.FAILED]: "failed",
19
+ [GstWebRTC.WebRTCPeerConnectionState.CLOSED]: "closed"
20
+ };
21
+ function gstToConnectionState(v) {
22
+ return CONNECTION_STATE_MAP[v] ?? "new";
23
+ }
24
+ const ICE_CONNECTION_STATE_MAP = {
25
+ [GstWebRTC.WebRTCICEConnectionState.NEW]: "new",
26
+ [GstWebRTC.WebRTCICEConnectionState.CHECKING]: "checking",
27
+ [GstWebRTC.WebRTCICEConnectionState.CONNECTED]: "connected",
28
+ [GstWebRTC.WebRTCICEConnectionState.COMPLETED]: "completed",
29
+ [GstWebRTC.WebRTCICEConnectionState.FAILED]: "failed",
30
+ [GstWebRTC.WebRTCICEConnectionState.DISCONNECTED]: "disconnected",
31
+ [GstWebRTC.WebRTCICEConnectionState.CLOSED]: "closed"
32
+ };
33
+ function gstToIceConnectionState(v) {
34
+ return ICE_CONNECTION_STATE_MAP[v] ?? "new";
35
+ }
36
+ const ICE_GATHERING_STATE_MAP = {
37
+ [GstWebRTC.WebRTCICEGatheringState.NEW]: "new",
38
+ [GstWebRTC.WebRTCICEGatheringState.GATHERING]: "gathering",
39
+ [GstWebRTC.WebRTCICEGatheringState.COMPLETE]: "complete"
40
+ };
41
+ function gstToIceGatheringState(v) {
42
+ return ICE_GATHERING_STATE_MAP[v] ?? "new";
43
+ }
44
+ const DIRECTION_GST_TO_W3C = {
45
+ [GstWebRTC.WebRTCRTPTransceiverDirection.SENDRECV]: "sendrecv",
46
+ [GstWebRTC.WebRTCRTPTransceiverDirection.SENDONLY]: "sendonly",
47
+ [GstWebRTC.WebRTCRTPTransceiverDirection.RECVONLY]: "recvonly"
48
+ };
49
+ const DIRECTION_W3C_TO_GST = {
50
+ sendrecv: GstWebRTC.WebRTCRTPTransceiverDirection.SENDRECV,
51
+ sendonly: GstWebRTC.WebRTCRTPTransceiverDirection.SENDONLY,
52
+ recvonly: GstWebRTC.WebRTCRTPTransceiverDirection.RECVONLY,
53
+ inactive: GstWebRTC.WebRTCRTPTransceiverDirection.NONE
54
+ };
55
+ function gstDirectionToW3C(v) {
56
+ return DIRECTION_GST_TO_W3C[v] ?? "inactive";
57
+ }
58
+ function w3cDirectionToGst(d) {
59
+ return DIRECTION_W3C_TO_GST[d] ?? GstWebRTC.WebRTCRTPTransceiverDirection.NONE;
60
+ }
61
+ const STATS_TYPE_MAP = {
62
+ [GstWebRTC.WebRTCStatsType.CODEC]: "codec",
63
+ [GstWebRTC.WebRTCStatsType.INBOUND_RTP]: "inbound-rtp",
64
+ [GstWebRTC.WebRTCStatsType.OUTBOUND_RTP]: "outbound-rtp",
65
+ [GstWebRTC.WebRTCStatsType.REMOTE_INBOUND_RTP]: "remote-inbound-rtp",
66
+ [GstWebRTC.WebRTCStatsType.REMOTE_OUTBOUND_RTP]: "remote-outbound-rtp",
67
+ [GstWebRTC.WebRTCStatsType.CSRC]: "csrc",
68
+ [GstWebRTC.WebRTCStatsType.PEER_CONNECTION]: "peer-connection",
69
+ [GstWebRTC.WebRTCStatsType.DATA_CHANNEL]: "data-channel",
70
+ [GstWebRTC.WebRTCStatsType.STREAM]: "stream",
71
+ [GstWebRTC.WebRTCStatsType.TRANSPORT]: "transport",
72
+ [GstWebRTC.WebRTCStatsType.CANDIDATE_PAIR]: "candidate-pair",
73
+ [GstWebRTC.WebRTCStatsType.LOCAL_CANDIDATE]: "local-candidate",
74
+ [GstWebRTC.WebRTCStatsType.REMOTE_CANDIDATE]: "remote-candidate",
75
+ [GstWebRTC.WebRTCStatsType.CERTIFICATE]: "certificate"
76
+ };
77
+ function gstToStatsType(v) {
78
+ return STATS_TYPE_MAP[v];
79
+ }
80
+ export {
81
+ gstDirectionToW3C,
82
+ gstToConnectionState,
83
+ gstToIceConnectionState,
84
+ gstToIceGatheringState,
85
+ gstToSignalingState,
86
+ gstToStatsType,
87
+ w3cDirectionToGst
88
+ };
@@ -0,0 +1,34 @@
1
+ import Gst from "gi://Gst?version=1.0";
2
+ let initialized = false;
3
+ function ensureGstInit() {
4
+ if (initialized) return;
5
+ Gst.init(null);
6
+ initialized = true;
7
+ }
8
+ function ensureWebrtcbinAvailable() {
9
+ ensureGstInit();
10
+ const webrtcFactory = Gst.ElementFactory.find("webrtcbin");
11
+ if (!webrtcFactory) {
12
+ throwNotSupported(
13
+ 'GStreamer element "webrtcbin" not available. Install gst-plugins-bad:\n Fedora: dnf install gstreamer1-plugins-bad-free gstreamer1-plugins-bad-free-extras\n Ubuntu/Debian: apt install gstreamer1.0-plugins-bad'
14
+ );
15
+ }
16
+ const niceFactory = Gst.ElementFactory.find("nicesrc");
17
+ if (!niceFactory) {
18
+ throwNotSupported(
19
+ 'GStreamer "nice" plugin (libnice-gstreamer) not available \u2014 required by webrtcbin.\n Fedora: dnf install libnice-gstreamer1\n Ubuntu/Debian: apt install gstreamer1.0-nice\n Verify with: gst-inspect-1.0 nicesrc'
20
+ );
21
+ }
22
+ }
23
+ function throwNotSupported(message) {
24
+ const DOMExceptionCtor = globalThis.DOMException;
25
+ if (DOMExceptionCtor) {
26
+ throw new DOMExceptionCtor(message, "NotSupportedError");
27
+ }
28
+ throw new Error(message);
29
+ }
30
+ export {
31
+ Gst,
32
+ ensureGstInit,
33
+ ensureWebrtcbinAvailable
34
+ };
@@ -0,0 +1,79 @@
1
+ import { gstToStatsType } from "./gst-enum-maps.js";
2
+ import { RTCStatsReport } from "./rtc-stats-report.js";
3
+ function snakeToCamel(name) {
4
+ return name.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
5
+ }
6
+ function extractFields(structure) {
7
+ const result = {};
8
+ const n = structure.n_fields();
9
+ for (let i = 0; i < n; i++) {
10
+ const fieldName = structure.nth_field_name(i);
11
+ try {
12
+ const value = structure.get_value(fieldName);
13
+ result[fieldName] = value;
14
+ } catch {
15
+ }
16
+ }
17
+ return result;
18
+ }
19
+ function parseStatsEntry(structure) {
20
+ const raw = extractFields(structure);
21
+ const gstType = raw["type"];
22
+ if (gstType == null) return null;
23
+ const w3cType = gstToStatsType(Number(gstType));
24
+ if (!w3cType) return null;
25
+ const id = String(raw["id"] ?? structure.get_name() ?? "");
26
+ const tsRaw = raw["timestamp"];
27
+ const timestamp = tsRaw != null ? Number(tsRaw) / 1e6 : performance.now();
28
+ const stats = { type: w3cType, id, timestamp };
29
+ for (const [key, value] of Object.entries(raw)) {
30
+ if (key === "type" || key === "id" || key === "timestamp") continue;
31
+ const camelKey = snakeToCamel(key);
32
+ stats[camelKey] = value;
33
+ }
34
+ return stats;
35
+ }
36
+ function parseGstStats(reply) {
37
+ if (!reply) return new RTCStatsReport();
38
+ const entries = [];
39
+ const n = reply.n_fields();
40
+ for (let i = 0; i < n; i++) {
41
+ const fieldName = reply.nth_field_name(i);
42
+ try {
43
+ const nested = reply.get_value(fieldName);
44
+ if (nested && typeof nested.n_fields === "function") {
45
+ const stats = parseStatsEntry(nested);
46
+ if (stats) {
47
+ entries.push([stats.id, stats]);
48
+ }
49
+ }
50
+ } catch {
51
+ }
52
+ }
53
+ return new RTCStatsReport(entries);
54
+ }
55
+ function filterStatsByTrackId(report, trackId) {
56
+ const entries = [];
57
+ const relatedIds = /* @__PURE__ */ new Set();
58
+ for (const [id, stats] of report) {
59
+ if (stats.trackIdentifier === trackId) {
60
+ entries.push([id, stats]);
61
+ relatedIds.add(id);
62
+ for (const value of Object.values(stats)) {
63
+ if (typeof value === "string" && report.has(value)) {
64
+ relatedIds.add(value);
65
+ }
66
+ }
67
+ }
68
+ }
69
+ for (const [id, stats] of report) {
70
+ if (relatedIds.has(id) && !entries.some(([entryId]) => entryId === id)) {
71
+ entries.push([id, stats]);
72
+ }
73
+ }
74
+ return new RTCStatsReport(entries);
75
+ }
76
+ export {
77
+ filterStatsByTrackId,
78
+ parseGstStats
79
+ };
@@ -0,0 +1,16 @@
1
+ import { PromiseBridge } from "@gjsify/webrtc-native";
2
+ function withGstPromise(emit) {
3
+ return new Promise((resolve, reject) => {
4
+ const bridge = new PromiseBridge();
5
+ bridge.connect("replied", (_b, reply) => {
6
+ resolve(reply);
7
+ });
8
+ bridge.connect("rejected", (_b, message) => {
9
+ reject(new Error(message));
10
+ });
11
+ emit(bridge.promise);
12
+ });
13
+ }
14
+ export {
15
+ withGstPromise
16
+ };
@@ -0,0 +1,53 @@
1
+ import { RTCPeerConnection } from "./rtc-peer-connection.js";
2
+ import { RTCDataChannel } from "./rtc-data-channel.js";
3
+ import { RTCSessionDescription } from "./rtc-session-description.js";
4
+ import { RTCIceCandidate } from "./rtc-ice-candidate.js";
5
+ import { RTCError } from "./rtc-error.js";
6
+ import {
7
+ RTCPeerConnectionIceEvent,
8
+ RTCDataChannelEvent,
9
+ RTCErrorEvent
10
+ } from "./rtc-events.js";
11
+ import { RTCRtpSender } from "./rtc-rtp-sender.js";
12
+ import { RTCRtpReceiver } from "./rtc-rtp-receiver.js";
13
+ import { RTCRtpTransceiver } from "./rtc-rtp-transceiver.js";
14
+ import { MediaStream } from "./media-stream.js";
15
+ import { MediaStreamTrackEvent } from "./media-stream.js";
16
+ import { MediaStreamTrack } from "./media-stream-track.js";
17
+ import { RTCTrackEvent } from "./rtc-track-event.js";
18
+ import { getUserMedia } from "./get-user-media.js";
19
+ import { MediaDevices } from "./media-devices.js";
20
+ import { MediaDeviceInfo } from "./media-device-info.js";
21
+ import { RTCStatsReport } from "./rtc-stats-report.js";
22
+ import { RTCDtlsTransport } from "./rtc-dtls-transport.js";
23
+ import { RTCIceTransport } from "./rtc-ice-transport.js";
24
+ import { RTCSctpTransport } from "./rtc-sctp-transport.js";
25
+ import { RTCDTMFSender, RTCDTMFToneChangeEvent } from "./rtc-dtmf-sender.js";
26
+ import { RTCCertificate } from "./rtc-certificate.js";
27
+ export {
28
+ MediaDeviceInfo,
29
+ MediaDevices,
30
+ MediaStream,
31
+ MediaStreamTrack,
32
+ MediaStreamTrackEvent,
33
+ RTCCertificate,
34
+ RTCDTMFSender,
35
+ RTCDTMFToneChangeEvent,
36
+ RTCDataChannel,
37
+ RTCDataChannelEvent,
38
+ RTCDtlsTransport,
39
+ RTCError,
40
+ RTCErrorEvent,
41
+ RTCIceCandidate,
42
+ RTCIceTransport,
43
+ RTCPeerConnection,
44
+ RTCPeerConnectionIceEvent,
45
+ RTCRtpReceiver,
46
+ RTCRtpSender,
47
+ RTCRtpTransceiver,
48
+ RTCSctpTransport,
49
+ RTCSessionDescription,
50
+ RTCStatsReport,
51
+ RTCTrackEvent,
52
+ getUserMedia
53
+ };
@@ -0,0 +1,23 @@
1
+ class MediaDeviceInfo {
2
+ deviceId;
3
+ kind;
4
+ label;
5
+ groupId;
6
+ constructor(init) {
7
+ this.deviceId = init.deviceId;
8
+ this.kind = init.kind;
9
+ this.label = init.label;
10
+ this.groupId = init.groupId ?? "";
11
+ }
12
+ toJSON() {
13
+ return {
14
+ deviceId: this.deviceId,
15
+ kind: this.kind,
16
+ label: this.label,
17
+ groupId: this.groupId
18
+ };
19
+ }
20
+ }
21
+ export {
22
+ MediaDeviceInfo
23
+ };
@@ -0,0 +1,147 @@
1
+ import "@gjsify/dom-events/register/event-target";
2
+ import { ensureGstInit, Gst } from "./gst-init.js";
3
+ import { getUserMedia } from "./get-user-media.js";
4
+ import { MediaDeviceInfo } from "./media-device-info.js";
5
+ const DEVICE_CLASS_MAP = {
6
+ "Audio/Source": "audioinput",
7
+ "Video/Source": "videoinput",
8
+ "Audio/Sink": "audiooutput"
9
+ };
10
+ let _permissionGranted = false;
11
+ function isDeviceMonitorSafe() {
12
+ try {
13
+ const GLib = imports.gi.GLib;
14
+ if (GLib.getenv("CI")) return false;
15
+ if (!GLib.getenv("DISPLAY") && !GLib.getenv("WAYLAND_DISPLAY")) return false;
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ class MediaDevices extends EventTarget {
22
+ _ondevicechange = null;
23
+ get ondevicechange() {
24
+ return this._ondevicechange;
25
+ }
26
+ set ondevicechange(v) {
27
+ this._ondevicechange = v;
28
+ }
29
+ async getUserMedia(constraints) {
30
+ if (!constraints) {
31
+ throw new TypeError(
32
+ "Failed to execute 'getUserMedia' on 'MediaDevices': At least one of audio or video must be requested"
33
+ );
34
+ }
35
+ const stream = await getUserMedia(constraints);
36
+ _permissionGranted = true;
37
+ return stream;
38
+ }
39
+ async enumerateDevices() {
40
+ ensureGstInit();
41
+ let monitor = null;
42
+ const result = [];
43
+ try {
44
+ if (!isDeviceMonitorSafe()) {
45
+ return result;
46
+ }
47
+ monitor = new Gst.DeviceMonitor();
48
+ monitor.set_show_all_devices(true);
49
+ const audioCaps = Gst.Caps.from_string("audio/x-raw");
50
+ const videoCaps = Gst.Caps.from_string("video/x-raw");
51
+ if (audioCaps) {
52
+ monitor.add_filter("Audio/Source", audioCaps);
53
+ monitor.add_filter("Audio/Sink", audioCaps);
54
+ }
55
+ if (videoCaps) {
56
+ monitor.add_filter("Video/Source", videoCaps);
57
+ }
58
+ if (!monitor.start()) {
59
+ return result;
60
+ }
61
+ let gstDevices;
62
+ try {
63
+ gstDevices = monitor.get_devices() ?? [];
64
+ } catch {
65
+ return result;
66
+ }
67
+ for (const device of gstDevices) {
68
+ const deviceClass = device.get_device_class?.() ?? "";
69
+ const kind = DEVICE_CLASS_MAP[deviceClass];
70
+ if (!kind) continue;
71
+ const displayName = device.get_display_name?.() ?? "";
72
+ let deviceId = "";
73
+ let groupId = "";
74
+ try {
75
+ const props = device.get_properties?.();
76
+ if (props) {
77
+ const n = props.n_fields();
78
+ for (let i = 0; i < n; i++) {
79
+ const name = props.nth_field_name(i);
80
+ if (name === "persistent-id" || name === "node.name") {
81
+ const val = props.get_value(name);
82
+ if (val && !deviceId) deviceId = String(val);
83
+ }
84
+ if (name === "group-id") {
85
+ const val = props.get_value(name);
86
+ if (val) groupId = String(val);
87
+ }
88
+ }
89
+ }
90
+ } catch {
91
+ }
92
+ if (!deviceId) {
93
+ deviceId = displayName || `${kind}-${result.length}`;
94
+ }
95
+ if (_permissionGranted) {
96
+ result.push(new MediaDeviceInfo({
97
+ deviceId,
98
+ kind,
99
+ label: displayName,
100
+ groupId
101
+ }));
102
+ } else {
103
+ if (!result.some((d) => d.kind === kind)) {
104
+ result.push(new MediaDeviceInfo({
105
+ deviceId: "",
106
+ kind,
107
+ label: "",
108
+ groupId: ""
109
+ }));
110
+ }
111
+ }
112
+ }
113
+ } catch {
114
+ return result;
115
+ } finally {
116
+ try {
117
+ monitor?.stop();
118
+ } catch {
119
+ }
120
+ }
121
+ const order = { audioinput: 0, videoinput: 1, audiooutput: 2 };
122
+ result.sort((a, b) => (order[a.kind] ?? 3) - (order[b.kind] ?? 3));
123
+ return result;
124
+ }
125
+ getSupportedConstraints() {
126
+ return {
127
+ deviceId: true,
128
+ width: true,
129
+ height: true,
130
+ frameRate: true,
131
+ sampleRate: true,
132
+ channelCount: true,
133
+ // Not yet supported — return false
134
+ aspectRatio: false,
135
+ facingMode: false,
136
+ resizeMode: false,
137
+ echoCancellation: false,
138
+ autoGainControl: false,
139
+ noiseSuppression: false,
140
+ latency: false,
141
+ groupId: false
142
+ };
143
+ }
144
+ }
145
+ export {
146
+ MediaDevices
147
+ };
@@ -0,0 +1,142 @@
1
+ import "@gjsify/dom-events/register/event-target";
2
+ import GLib from "gi://GLib?version=2.0";
3
+ import { Gst } from "./gst-init.js";
4
+ class MediaStreamTrack extends EventTarget {
5
+ id;
6
+ kind;
7
+ label;
8
+ _enabled = true;
9
+ _muted;
10
+ _ended = false;
11
+ _contentHint = "";
12
+ _onended = null;
13
+ _onmute = null;
14
+ _onunmute = null;
15
+ /** @internal GStreamer source element (e.g. pulsesrc, audiotestsrc) */
16
+ _gstSource = null;
17
+ /** @internal Pipeline the source currently lives in (updated by VideoBridge) */
18
+ _gstPipeline = null;
19
+ /** @internal Tee element inserted by VideoBridge for preview fan-out */
20
+ _gstTee = null;
21
+ /** @internal TeeMultiplexer for multi-PC fan-out (created on second addTrack) */
22
+ _teeMultiplexer = null;
23
+ /** @internal Callback set by RTCRtpSender to control valve drop property */
24
+ _enableCallback = null;
25
+ constructor(init) {
26
+ super();
27
+ this.id = init.id ?? GLib.uuid_string_random();
28
+ this.kind = init.kind;
29
+ this.label = init.label ?? "";
30
+ this._muted = init.muted ?? false;
31
+ if (init._gst) {
32
+ this._gstSource = init._gst.source;
33
+ this._gstPipeline = init._gst.pipeline;
34
+ }
35
+ }
36
+ get enabled() {
37
+ return this._enabled;
38
+ }
39
+ set enabled(v) {
40
+ const val = !!v;
41
+ if (this._enabled === val) return;
42
+ this._enabled = val;
43
+ this._enableCallback?.(val);
44
+ }
45
+ get muted() {
46
+ return this._muted;
47
+ }
48
+ get readyState() {
49
+ return this._ended ? "ended" : "live";
50
+ }
51
+ get contentHint() {
52
+ return this._contentHint;
53
+ }
54
+ set contentHint(v) {
55
+ if (this.kind === "audio") {
56
+ if (v !== "" && v !== "speech" && v !== "speech-recognition" && v !== "music") return;
57
+ } else {
58
+ if (v !== "" && v !== "motion" && v !== "detail" && v !== "text") return;
59
+ }
60
+ this._contentHint = v;
61
+ }
62
+ get onended() {
63
+ return this._onended;
64
+ }
65
+ set onended(v) {
66
+ this._onended = v;
67
+ }
68
+ get onmute() {
69
+ return this._onmute;
70
+ }
71
+ set onmute(v) {
72
+ this._onmute = v;
73
+ }
74
+ get onunmute() {
75
+ return this._onunmute;
76
+ }
77
+ set onunmute(v) {
78
+ this._onunmute = v;
79
+ }
80
+ clone() {
81
+ const cloned = new MediaStreamTrack({
82
+ kind: this.kind,
83
+ label: this.label,
84
+ muted: this._muted
85
+ });
86
+ cloned._enabled = this._enabled;
87
+ return cloned;
88
+ }
89
+ stop() {
90
+ if (this._ended) return;
91
+ this._ended = true;
92
+ if (this._gstSource || this._gstPipeline) {
93
+ try {
94
+ this._gstPipeline?.set_state(Gst.State.NULL);
95
+ } catch {
96
+ }
97
+ try {
98
+ this._gstSource?.set_state(Gst.State.NULL);
99
+ } catch {
100
+ }
101
+ this._gstSource = null;
102
+ this._gstPipeline = null;
103
+ }
104
+ const ev = new Event("ended");
105
+ this._onended?.call(this, ev);
106
+ this.dispatchEvent(ev);
107
+ }
108
+ getCapabilities() {
109
+ return {};
110
+ }
111
+ getConstraints() {
112
+ return {};
113
+ }
114
+ getSettings() {
115
+ return {};
116
+ }
117
+ applyConstraints(_constraints) {
118
+ return Promise.reject(new DOMException(
119
+ "applyConstraints is not supported",
120
+ "NotSupportedError"
121
+ ));
122
+ }
123
+ /** @internal — used by RTCRtpReceiver to toggle mute state */
124
+ _setMuted(muted) {
125
+ if (this._muted === muted) return;
126
+ this._muted = muted;
127
+ const ev = new Event(muted ? "mute" : "unmute");
128
+ if (muted) {
129
+ this._onmute?.call(this, ev);
130
+ } else {
131
+ this._onunmute?.call(this, ev);
132
+ }
133
+ this.dispatchEvent(ev);
134
+ }
135
+ /** @internal — called by RTCRtpSender to wire valve control */
136
+ _setEnableCallback(cb) {
137
+ this._enableCallback = cb;
138
+ }
139
+ }
140
+ export {
141
+ MediaStreamTrack
142
+ };