@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,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
|
+
};
|
package/lib/esm/index.js
ADDED
|
@@ -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
|
+
};
|