@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.
- package/package.json +73 -70
- package/src/get-user-media.ts +0 -131
- package/src/gst-enum-maps.ts +0 -125
- package/src/gst-init.ts +0 -49
- package/src/gst-stats-parser.ts +0 -137
- package/src/gst-utils.ts +0 -41
- package/src/index.ts +0 -104
- package/src/internal/gst-types.ts +0 -122
- package/src/media-device-info.ts +0 -33
- package/src/media-devices.ts +0 -191
- package/src/media-stream-track.ts +0 -159
- package/src/media-stream.ts +0 -96
- package/src/register/data-channel.ts +0 -11
- package/src/register/error.ts +0 -11
- package/src/register/media-devices.ts +0 -10
- package/src/register/media.ts +0 -15
- package/src/register/peer-connection.ts +0 -20
- package/src/register.spec.ts +0 -55
- package/src/register.ts +0 -10
- package/src/rtc-certificate.ts +0 -110
- package/src/rtc-data-channel.ts +0 -283
- package/src/rtc-dtls-transport.ts +0 -48
- package/src/rtc-dtmf-sender.ts +0 -146
- package/src/rtc-error.ts +0 -49
- package/src/rtc-events.ts +0 -64
- package/src/rtc-ice-candidate.ts +0 -115
- package/src/rtc-ice-transport.ts +0 -104
- package/src/rtc-peer-connection.ts +0 -1039
- package/src/rtc-rtp-receiver.ts +0 -122
- package/src/rtc-rtp-sender.ts +0 -471
- package/src/rtc-rtp-transceiver.ts +0 -131
- package/src/rtc-sctp-transport.ts +0 -48
- package/src/rtc-session-description.ts +0 -64
- package/src/rtc-stats-report.ts +0 -39
- package/src/rtc-track-event.ts +0 -45
- package/src/rtp-capabilities.ts +0 -48
- package/src/tee-multiplexer.ts +0 -75
- package/src/test.mts +0 -11
- package/src/webrtc.spec.ts +0 -1186
- package/src/wpt-helpers.ts +0 -156
- package/src/wpt-media.spec.ts +0 -1154
- package/src/wpt.spec.ts +0 -1136
- package/tsconfig.json +0 -36
- package/tsconfig.tsbuildinfo +0 -1
package/src/index.ts
DELETED
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
// W3C WebRTC API for GJS — backed by GStreamer webrtcbin.
|
|
2
|
-
//
|
|
3
|
-
// This module has no side effects. Importing @gjsify/webrtc gives named
|
|
4
|
-
// access to the classes but does NOT register globals. Use
|
|
5
|
-
// @gjsify/webrtc/register (or a granular subpath) to set globalThis.RTCPeerConnection etc.
|
|
6
|
-
|
|
7
|
-
export { RTCPeerConnection } from './rtc-peer-connection.js';
|
|
8
|
-
export type {
|
|
9
|
-
RTCConfiguration,
|
|
10
|
-
RTCIceServer,
|
|
11
|
-
RTCOfferOptions,
|
|
12
|
-
RTCAnswerOptions,
|
|
13
|
-
RTCDataChannelInit,
|
|
14
|
-
RTCSignalingState,
|
|
15
|
-
RTCPeerConnectionState,
|
|
16
|
-
RTCIceConnectionState,
|
|
17
|
-
RTCIceGatheringState,
|
|
18
|
-
RTCIceTransportPolicy,
|
|
19
|
-
RTCBundlePolicy,
|
|
20
|
-
RTCRtcpMuxPolicy,
|
|
21
|
-
} from './rtc-peer-connection.js';
|
|
22
|
-
|
|
23
|
-
export { RTCDataChannel } from './rtc-data-channel.js';
|
|
24
|
-
export type { RTCDataChannelState, BinaryType } from './rtc-data-channel.js';
|
|
25
|
-
|
|
26
|
-
export { RTCSessionDescription } from './rtc-session-description.js';
|
|
27
|
-
export type { RTCSessionDescriptionInit, RTCSdpType } from './rtc-session-description.js';
|
|
28
|
-
|
|
29
|
-
export { RTCIceCandidate } from './rtc-ice-candidate.js';
|
|
30
|
-
export type {
|
|
31
|
-
RTCIceCandidateInit,
|
|
32
|
-
RTCIceComponent,
|
|
33
|
-
RTCIceProtocol,
|
|
34
|
-
RTCIceCandidateType,
|
|
35
|
-
RTCIceTcpCandidateType,
|
|
36
|
-
} from './rtc-ice-candidate.js';
|
|
37
|
-
|
|
38
|
-
export { RTCError } from './rtc-error.js';
|
|
39
|
-
export type { RTCErrorInit, RTCErrorDetailType } from './rtc-error.js';
|
|
40
|
-
|
|
41
|
-
export {
|
|
42
|
-
RTCPeerConnectionIceEvent,
|
|
43
|
-
RTCDataChannelEvent,
|
|
44
|
-
RTCErrorEvent,
|
|
45
|
-
} from './rtc-events.js';
|
|
46
|
-
export type {
|
|
47
|
-
RTCPeerConnectionIceEventInit,
|
|
48
|
-
RTCDataChannelEventInit,
|
|
49
|
-
RTCErrorEventInit,
|
|
50
|
-
} from './rtc-events.js';
|
|
51
|
-
|
|
52
|
-
export { RTCRtpSender } from './rtc-rtp-sender.js';
|
|
53
|
-
export type {
|
|
54
|
-
RTCRtpTransceiverDirection,
|
|
55
|
-
RTCRtpCapabilities,
|
|
56
|
-
RTCRtpCodecCapability,
|
|
57
|
-
RTCRtpHeaderExtensionCapability,
|
|
58
|
-
RTCRtpSendParameters,
|
|
59
|
-
RTCRtpEncodingParameters,
|
|
60
|
-
RTCRtpCodecParameters,
|
|
61
|
-
RTCRtpHeaderExtensionParameters,
|
|
62
|
-
RTCRtcpParameters,
|
|
63
|
-
} from './rtc-rtp-sender.js';
|
|
64
|
-
|
|
65
|
-
export { RTCRtpReceiver } from './rtc-rtp-receiver.js';
|
|
66
|
-
export type { RTCRtpReceiveParameters } from './rtc-rtp-receiver.js';
|
|
67
|
-
|
|
68
|
-
export { RTCRtpTransceiver } from './rtc-rtp-transceiver.js';
|
|
69
|
-
|
|
70
|
-
export { MediaStream } from './media-stream.js';
|
|
71
|
-
export { MediaStreamTrackEvent } from './media-stream.js';
|
|
72
|
-
|
|
73
|
-
export { MediaStreamTrack } from './media-stream-track.js';
|
|
74
|
-
export type { MediaStreamTrackInit } from './media-stream-track.js';
|
|
75
|
-
|
|
76
|
-
export { RTCTrackEvent } from './rtc-track-event.js';
|
|
77
|
-
export type { RTCTrackEventInit } from './rtc-track-event.js';
|
|
78
|
-
|
|
79
|
-
export type { RTCRtpTransceiverInit } from './rtc-peer-connection.js';
|
|
80
|
-
|
|
81
|
-
export { getUserMedia } from './get-user-media.js';
|
|
82
|
-
export type { MediaStreamConstraints, MediaTrackConstraints } from './get-user-media.js';
|
|
83
|
-
|
|
84
|
-
export { MediaDevices } from './media-devices.js';
|
|
85
|
-
export { MediaDeviceInfo } from './media-device-info.js';
|
|
86
|
-
export type { MediaDeviceKind } from './media-device-info.js';
|
|
87
|
-
|
|
88
|
-
export { RTCStatsReport } from './rtc-stats-report.js';
|
|
89
|
-
export type { RTCStats } from './rtc-stats-report.js';
|
|
90
|
-
|
|
91
|
-
export { RTCDtlsTransport } from './rtc-dtls-transport.js';
|
|
92
|
-
export type { RTCDtlsTransportState } from './rtc-dtls-transport.js';
|
|
93
|
-
|
|
94
|
-
export { RTCIceTransport } from './rtc-ice-transport.js';
|
|
95
|
-
export type { RTCIceTransportState, RTCIceRole, RTCIceParameters, RTCIceCandidatePair } from './rtc-ice-transport.js';
|
|
96
|
-
|
|
97
|
-
export { RTCSctpTransport } from './rtc-sctp-transport.js';
|
|
98
|
-
export type { RTCSctpTransportState } from './rtc-sctp-transport.js';
|
|
99
|
-
|
|
100
|
-
export { RTCDTMFSender, RTCDTMFToneChangeEvent } from './rtc-dtmf-sender.js';
|
|
101
|
-
export type { RTCDTMFToneChangeEventInit } from './rtc-dtmf-sender.js';
|
|
102
|
-
|
|
103
|
-
export { RTCCertificate } from './rtc-certificate.js';
|
|
104
|
-
export type { RTCDtlsFingerprint, AlgorithmIdentifier } from './rtc-certificate.js';
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Internal type helpers for @gjsify/webrtc — narrowing the broad
|
|
3
|
-
// Gst.Element / Gst.Pad surface to the concrete element shapes our
|
|
4
|
-
// implementation reads and writes at runtime.
|
|
5
|
-
//
|
|
6
|
-
// Background: `@girs/gst-1.0` declares every GStreamer element as the
|
|
7
|
-
// base `Gst.Element` class — element-specific GObject properties (e.g.
|
|
8
|
-
// `webrtcbin`'s `stun_server`, `signaling_state`, or `vp8enc`'s
|
|
9
|
-
// `keyframe_max_dist`) are not exposed in the GIR-generated typings
|
|
10
|
-
// because they are registered at element-class init time, not on the
|
|
11
|
-
// base class. Rather than reach for `(el as any).foo` at every call
|
|
12
|
-
// site, we declare thin interfaces here and narrow once through helper
|
|
13
|
-
// casts.
|
|
14
|
-
//
|
|
15
|
-
// These types are PURE compile-time constructs — no runtime behavior is
|
|
16
|
-
// added. Following AGENTS.md Rule 2c, this module lives under
|
|
17
|
-
// `src/internal/` and is NOT in `package.json#exports`; it is a private
|
|
18
|
-
// implementation helper, not part of the public API surface.
|
|
19
|
-
//
|
|
20
|
-
// References:
|
|
21
|
-
// - GStreamer webrtcbin element properties:
|
|
22
|
-
// https://gstreamer.freedesktop.org/documentation/webrtc/index.html
|
|
23
|
-
// - vp8enc / opusenc / capsfilter / valve / payloader properties:
|
|
24
|
-
// https://gstreamer.freedesktop.org/documentation/
|
|
25
|
-
|
|
26
|
-
import type Gst from 'gi://Gst?version=1.0';
|
|
27
|
-
import type GstWebRTC from 'gi://GstWebRTC?version=1.0';
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* The `webrtcbin` GStreamer element — declares every GObject property
|
|
31
|
-
* our implementation accesses. The base `Gst.Element` class is preserved
|
|
32
|
-
* so all the inherited methods (`emit`, `get_parent`, `set_state`,
|
|
33
|
-
* `request_pad_simple`, `sync_state_with_parent`, …) keep their proper
|
|
34
|
-
* typings.
|
|
35
|
-
*
|
|
36
|
-
* All properties below correspond to runtime GObject properties on
|
|
37
|
-
* `webrtcbin` (verify with `gst-inspect-1.0 webrtcbin`).
|
|
38
|
-
*/
|
|
39
|
-
export interface WebRtcBin extends Gst.Element {
|
|
40
|
-
stun_server: string | null;
|
|
41
|
-
turn_server: string | null;
|
|
42
|
-
ice_transport_policy: GstWebRTC.WebRTCICETransportPolicy;
|
|
43
|
-
bundle_policy: GstWebRTC.WebRTCBundlePolicy;
|
|
44
|
-
signaling_state: GstWebRTC.WebRTCSignalingState;
|
|
45
|
-
connection_state: GstWebRTC.WebRTCPeerConnectionState;
|
|
46
|
-
ice_connection_state: GstWebRTC.WebRTCICEConnectionState;
|
|
47
|
-
ice_gathering_state: GstWebRTC.WebRTCICEGatheringState;
|
|
48
|
-
local_description: GstWebRTC.WebRTCSessionDescription | null;
|
|
49
|
-
remote_description: GstWebRTC.WebRTCSessionDescription | null;
|
|
50
|
-
current_local_description: GstWebRTC.WebRTCSessionDescription | null;
|
|
51
|
-
current_remote_description: GstWebRTC.WebRTCSessionDescription | null;
|
|
52
|
-
pending_local_description: GstWebRTC.WebRTCSessionDescription | null;
|
|
53
|
-
pending_remote_description: GstWebRTC.WebRTCSessionDescription | null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Narrow a `Gst.Element` returned by `Gst.ElementFactory.make('webrtcbin', …)`
|
|
58
|
-
* to the augmented `WebRtcBin` shape. Pure type-level cast — no runtime
|
|
59
|
-
* validation is performed because every webrtcbin instance has these
|
|
60
|
-
* properties (they are class-installed by GstWebRTCBin).
|
|
61
|
-
*/
|
|
62
|
-
export const asWebRtcBin = (el: Gst.Element): WebRtcBin => el as WebRtcBin;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* GStreamer pad augmented with the `transceiver` field that webrtcbin's
|
|
66
|
-
* SRC pads carry. The base `Gst.Pad` typing has no concept of this — it
|
|
67
|
-
* is set at pad-creation time inside webrtcbin and points back to the
|
|
68
|
-
* `GstWebRTCRTPTransceiver` the pad serves.
|
|
69
|
-
*/
|
|
70
|
-
export interface WebRtcSrcPad extends Gst.Pad {
|
|
71
|
-
transceiver: GstWebRTC.WebRTCRTPTransceiver | null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** Narrow a webrtcbin SRC `Gst.Pad` to the augmented shape. */
|
|
75
|
-
export const asWebRtcSrcPad = (pad: Gst.Pad): WebRtcSrcPad => pad as WebRtcSrcPad;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* GStreamer element with a settable `drop` GObject property — the
|
|
79
|
-
* `valve` element used to gate media flow when a track is disabled.
|
|
80
|
-
*/
|
|
81
|
-
export interface ValveElement extends Gst.Element {
|
|
82
|
-
drop: boolean;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Narrow a `valve` element to the augmented shape. */
|
|
86
|
-
export const asValveElement = (el: Gst.Element): ValveElement => el as ValveElement;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* GStreamer element with a settable `pt` GObject property — RTP payloader
|
|
90
|
-
* elements (`rtpopuspay`, `rtpvp8pay`, …) accept the payload type.
|
|
91
|
-
*/
|
|
92
|
-
export interface RtpPayloaderElement extends Gst.Element {
|
|
93
|
-
pt: number;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** Narrow an RTP payloader element to the augmented shape. */
|
|
97
|
-
export const asRtpPayloaderElement = (el: Gst.Element): RtpPayloaderElement =>
|
|
98
|
-
el as RtpPayloaderElement;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* GStreamer `capsfilter` element — exposes a settable `caps` property
|
|
102
|
-
* that pins the negotiated caps of a pipeline branch.
|
|
103
|
-
*/
|
|
104
|
-
export interface CapsFilterElement extends Gst.Element {
|
|
105
|
-
caps: Gst.Caps;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** Narrow a `capsfilter` element to the augmented shape. */
|
|
109
|
-
export const asCapsFilterElement = (el: Gst.Element): CapsFilterElement =>
|
|
110
|
-
el as CapsFilterElement;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* GStreamer `vp8enc` encoder — the small set of properties we tune for
|
|
114
|
-
* realtime WebRTC video encoding.
|
|
115
|
-
*/
|
|
116
|
-
export interface Vp8EncElement extends Gst.Element {
|
|
117
|
-
deadline: number;
|
|
118
|
-
keyframe_max_dist: number;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Narrow a `vp8enc` element to the augmented shape. */
|
|
122
|
-
export const asVp8EncElement = (el: Gst.Element): Vp8EncElement => el as Vp8EncElement;
|
package/src/media-device-info.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
// W3C MediaDeviceInfo for GJS — backed by GStreamer Device Monitor.
|
|
2
|
-
//
|
|
3
|
-
// Reference: W3C Media Capture and Streams spec § 10.2.1
|
|
4
|
-
|
|
5
|
-
export type MediaDeviceKind = 'audioinput' | 'audiooutput' | 'videoinput';
|
|
6
|
-
|
|
7
|
-
export class MediaDeviceInfo {
|
|
8
|
-
readonly deviceId: string;
|
|
9
|
-
readonly kind: MediaDeviceKind;
|
|
10
|
-
readonly label: string;
|
|
11
|
-
readonly groupId: string;
|
|
12
|
-
|
|
13
|
-
constructor(init: {
|
|
14
|
-
deviceId: string;
|
|
15
|
-
kind: MediaDeviceKind;
|
|
16
|
-
label: string;
|
|
17
|
-
groupId?: string;
|
|
18
|
-
}) {
|
|
19
|
-
this.deviceId = init.deviceId;
|
|
20
|
-
this.kind = init.kind;
|
|
21
|
-
this.label = init.label;
|
|
22
|
-
this.groupId = init.groupId ?? '';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
toJSON(): object {
|
|
26
|
-
return {
|
|
27
|
-
deviceId: this.deviceId,
|
|
28
|
-
kind: this.kind,
|
|
29
|
-
label: this.label,
|
|
30
|
-
groupId: this.groupId,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
}
|
package/src/media-devices.ts
DELETED
|
@@ -1,191 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,159 +0,0 @@
|
|
|
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
|
-
}
|