@gjsify/webrtc-native 0.3.21 → 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
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
2
|
+
"name": "@gjsify/webrtc-native",
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"description": "Vala-based main-thread signal bridge for @gjsify/webrtc. Marshals webrtcbin signals and GstPromise callbacks from GStreamer's streaming thread onto the GLib main context so GJS can safely handle them.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/esm/index.js",
|
|
7
|
+
"module": "lib/esm/index.js",
|
|
8
|
+
"types": "lib/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./lib/types/index.d.ts",
|
|
12
|
+
"default": "./lib/esm/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"lib",
|
|
17
|
+
"prebuilds",
|
|
18
|
+
"meson.build",
|
|
19
|
+
"src/vala"
|
|
20
|
+
],
|
|
21
|
+
"gjsify": {
|
|
22
|
+
"prebuilds": "prebuilds"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"clear": "rm -rf lib build tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo || exit 0",
|
|
26
|
+
"check": "tsc --noEmit",
|
|
27
|
+
"init:meson": "meson setup build .",
|
|
28
|
+
"init:meson:wipe": "gjsify run init:meson --wipe",
|
|
29
|
+
"build": "gjsify run build:gjsify && gjsify run build:types",
|
|
30
|
+
"build:gjsify": "gjsify build --library 'src/ts/**/*.{ts,js}'",
|
|
31
|
+
"build:meson": "gjsify run init:meson && meson compile -C build",
|
|
32
|
+
"build:types": "tsc -b --force",
|
|
33
|
+
"build:prebuilds": "gjsify run build:meson && mkdir -p prebuilds/linux-x86_64 && cp build/libgjsifywebrtc.so build/GjsifyWebrtc-0.1.typelib prebuilds/linux-x86_64/"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"gjs",
|
|
37
|
+
"webrtc",
|
|
38
|
+
"gstreamer",
|
|
39
|
+
"webrtcbin",
|
|
40
|
+
"vala",
|
|
41
|
+
"native"
|
|
42
|
+
],
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@girs/gjs": "4.0.0-rc.15",
|
|
45
|
+
"@girs/gjsifywebrtc-0.1": "0.1.0-4.0.0-rc.5",
|
|
46
|
+
"@girs/glib-2.0": "2.88.0-4.0.0-rc.15",
|
|
47
|
+
"@girs/gobject-2.0": "2.88.0-4.0.0-rc.15",
|
|
48
|
+
"@girs/gst-1.0": "1.28.1-4.0.0-rc.15",
|
|
49
|
+
"@girs/gstwebrtc-1.0": "1.0.0-4.0.0-rc.15"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@gjsify/cli": "workspace:^",
|
|
53
|
+
"@types/node": "^25.6.2",
|
|
54
|
+
"typescript": "^6.0.3"
|
|
13
55
|
}
|
|
14
|
-
|
|
15
|
-
"files": [
|
|
16
|
-
"lib",
|
|
17
|
-
"prebuilds",
|
|
18
|
-
"meson.build",
|
|
19
|
-
"src/vala"
|
|
20
|
-
],
|
|
21
|
-
"gjsify": {
|
|
22
|
-
"prebuilds": "prebuilds"
|
|
23
|
-
},
|
|
24
|
-
"scripts": {
|
|
25
|
-
"clear": "rm -rf lib build tsconfig.tsbuildinfo tsconfig.types.tsbuildinfo || exit 0",
|
|
26
|
-
"check": "tsc --noEmit",
|
|
27
|
-
"init:meson": "meson setup build .",
|
|
28
|
-
"init:meson:wipe": "yarn init:meson --wipe",
|
|
29
|
-
"build": "yarn build:gjsify && yarn build:types",
|
|
30
|
-
"build:gjsify": "gjsify build --library 'src/ts/**/*.{ts,js}'",
|
|
31
|
-
"build:meson": "yarn init:meson && meson compile -C build",
|
|
32
|
-
"build:types": "tsc -b --force",
|
|
33
|
-
"build:prebuilds": "yarn build:meson && mkdir -p prebuilds/linux-x86_64 && cp build/libgjsifywebrtc.so build/GjsifyWebrtc-0.1.typelib prebuilds/linux-x86_64/"
|
|
34
|
-
},
|
|
35
|
-
"keywords": [
|
|
36
|
-
"gjs",
|
|
37
|
-
"webrtc",
|
|
38
|
-
"gstreamer",
|
|
39
|
-
"webrtcbin",
|
|
40
|
-
"vala",
|
|
41
|
-
"native"
|
|
42
|
-
],
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"@girs/gjs": "4.0.0-rc.14",
|
|
45
|
-
"@girs/gjsifywebrtc-0.1": "0.1.0-4.0.0-rc.5",
|
|
46
|
-
"@girs/glib-2.0": "2.88.0-4.0.0-rc.14",
|
|
47
|
-
"@girs/gobject-2.0": "2.88.0-4.0.0-rc.14",
|
|
48
|
-
"@girs/gst-1.0": "1.28.1-4.0.0-rc.14",
|
|
49
|
-
"@girs/gstwebrtc-1.0": "1.0.0-4.0.0-rc.14"
|
|
50
|
-
},
|
|
51
|
-
"devDependencies": {
|
|
52
|
-
"@gjsify/cli": "^0.3.21",
|
|
53
|
-
"@types/node": "^25.6.2",
|
|
54
|
-
"typescript": "^6.0.3"
|
|
55
|
-
}
|
|
56
|
-
}
|
|
56
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* DataChannelBridge — GstWebRTCDataChannel signals → main-thread signals.
|
|
3
|
+
*
|
|
4
|
+
* Same rationale as WebrtcbinBridge: GStreamer data-channel signals
|
|
5
|
+
* (on-open / on-close / on-error / on-message-string /
|
|
6
|
+
* on-message-data / on-buffered-amount-low) fire on the streaming
|
|
7
|
+
* thread, GJS blocks cross-thread JS callbacks, so we mirror them
|
|
8
|
+
* through a GLib.Idle.add() hop.
|
|
9
|
+
*
|
|
10
|
+
* Usage from JS:
|
|
11
|
+
* const bridge = new GjsifyWebrtc.DataChannelBridge(gstDataChannel);
|
|
12
|
+
* bridge.connect('message-string', (_b, str) => { ... });
|
|
13
|
+
* bridge.connect('message-data', (_b, bytes) => { ... });
|
|
14
|
+
*/
|
|
15
|
+
namespace GjsifyWebrtc {
|
|
16
|
+
|
|
17
|
+
public class DataChannelBridge : GLib.Object {
|
|
18
|
+
|
|
19
|
+
public Gst.WebRTCDataChannel channel { get; construct; }
|
|
20
|
+
|
|
21
|
+
/** on-open */
|
|
22
|
+
public signal void opened();
|
|
23
|
+
/** on-close */
|
|
24
|
+
public signal void closed();
|
|
25
|
+
/** on-error(GLib.Error) — delivered as the message string */
|
|
26
|
+
public signal void error_occurred(string message);
|
|
27
|
+
/** on-message-string(string) */
|
|
28
|
+
public signal void message_string(string data);
|
|
29
|
+
/** on-message-data(GLib.Bytes) */
|
|
30
|
+
public signal void message_data(GLib.Bytes data);
|
|
31
|
+
/** on-buffered-amount-low */
|
|
32
|
+
public signal void buffered_amount_low();
|
|
33
|
+
/** notify::ready-state — fires on state transitions (connecting/open/closing/closed) */
|
|
34
|
+
public signal void ready_state_changed();
|
|
35
|
+
|
|
36
|
+
private ulong[] _handler_ids = {};
|
|
37
|
+
|
|
38
|
+
public DataChannelBridge(Gst.WebRTCDataChannel channel) {
|
|
39
|
+
Object(channel: channel);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
construct {
|
|
43
|
+
_handler_ids += channel.on_open.connect(this.handle_open);
|
|
44
|
+
_handler_ids += channel.on_close.connect(this.handle_close);
|
|
45
|
+
_handler_ids += channel.on_error.connect(this.handle_error);
|
|
46
|
+
_handler_ids += channel.on_message_string.connect(this.handle_message_string);
|
|
47
|
+
_handler_ids += channel.on_message_data.connect(this.handle_message_data);
|
|
48
|
+
_handler_ids += channel.on_buffered_amount_low.connect(this.handle_buffered_amount_low);
|
|
49
|
+
_handler_ids += channel.notify["ready-state"].connect(this.handle_ready_state_changed);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private void handle_open() {
|
|
53
|
+
GLib.Idle.add(() => {
|
|
54
|
+
this.opened();
|
|
55
|
+
return false;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private void handle_close() {
|
|
60
|
+
GLib.Idle.add(() => {
|
|
61
|
+
this.closed();
|
|
62
|
+
return false;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private void handle_error(GLib.Error err) {
|
|
67
|
+
string msg = (err != null && err.message != null) ? err.message : "RTCDataChannel error";
|
|
68
|
+
GLib.Idle.add(() => {
|
|
69
|
+
this.error_occurred(msg);
|
|
70
|
+
return false;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private void handle_message_string(string? str) {
|
|
75
|
+
string payload = str ?? "";
|
|
76
|
+
GLib.Idle.add(() => {
|
|
77
|
+
this.message_string(payload);
|
|
78
|
+
return false;
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private void handle_message_data(GLib.Bytes? data) {
|
|
83
|
+
GLib.Bytes payload = data ?? new GLib.Bytes(new uint8[0]);
|
|
84
|
+
GLib.Idle.add(() => {
|
|
85
|
+
this.message_data(payload);
|
|
86
|
+
return false;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private void handle_buffered_amount_low() {
|
|
91
|
+
GLib.Idle.add(() => {
|
|
92
|
+
this.buffered_amount_low();
|
|
93
|
+
return false;
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private void handle_ready_state_changed(GLib.ParamSpec pspec) {
|
|
98
|
+
GLib.Idle.add(() => {
|
|
99
|
+
this.ready_state_changed();
|
|
100
|
+
return false;
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Disconnect all handlers from the wrapped channel. */
|
|
105
|
+
public void dispose_bridge() {
|
|
106
|
+
foreach (ulong id in _handler_ids) {
|
|
107
|
+
if (GLib.SignalHandler.is_connected(channel, id)) {
|
|
108
|
+
GLib.SignalHandler.disconnect(channel, id);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
_handler_ids = {};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PromiseBridge — GstPromise → main-thread signal.
|
|
3
|
+
*
|
|
4
|
+
* Gst.Promise.new_with_change_func() invokes its callback on the
|
|
5
|
+
* GStreamer streaming thread. GJS blocks cross-thread JS callbacks to
|
|
6
|
+
* prevent SpiderMonkey VM corruption, so the bridge catches the reply
|
|
7
|
+
* on the C side, hops to the GLib main context via GLib.Idle.add(),
|
|
8
|
+
* and only then emits the `replied` / `rejected` signals — which JS
|
|
9
|
+
* can safely handle.
|
|
10
|
+
*
|
|
11
|
+
* Usage from JS:
|
|
12
|
+
* const pb = new GjsifyWebrtc.PromiseBridge();
|
|
13
|
+
* pb.connect('replied', (_self, reply) => resolve(reply));
|
|
14
|
+
* pb.connect('rejected', (_self, msg) => reject(new Error(msg)));
|
|
15
|
+
* webrtcbin.emit('create-offer', opts, pb.promise);
|
|
16
|
+
*/
|
|
17
|
+
namespace GjsifyWebrtc {
|
|
18
|
+
|
|
19
|
+
public class PromiseBridge : GLib.Object {
|
|
20
|
+
|
|
21
|
+
/** Emitted on the main thread when the GstPromise replied successfully. */
|
|
22
|
+
public signal void replied(Gst.Structure? reply);
|
|
23
|
+
|
|
24
|
+
/** Emitted on the main thread when the GstPromise failed or expired. */
|
|
25
|
+
public signal void rejected(string message);
|
|
26
|
+
|
|
27
|
+
private Gst.Promise _promise;
|
|
28
|
+
|
|
29
|
+
/** Consumer passes this to webrtcbin.emit('create-offer', opts, promise). */
|
|
30
|
+
public Gst.Promise promise { get { return _promise; } }
|
|
31
|
+
|
|
32
|
+
construct {
|
|
33
|
+
// Keep a strong ref to `this` across the async hop.
|
|
34
|
+
var self = this;
|
|
35
|
+
_promise = new Gst.Promise.with_change_func((p) => {
|
|
36
|
+
var result = p.wait();
|
|
37
|
+
|
|
38
|
+
// Copy the reply off the promise so we can reach it from
|
|
39
|
+
// the main-thread handler without holding the streaming-thread's
|
|
40
|
+
// ownership model.
|
|
41
|
+
Gst.Structure? reply_copy = null;
|
|
42
|
+
string? error_message = null;
|
|
43
|
+
|
|
44
|
+
if (result == Gst.PromiseResult.REPLIED) {
|
|
45
|
+
unowned Gst.Structure? reply = p.get_reply();
|
|
46
|
+
if (reply != null) {
|
|
47
|
+
reply_copy = reply.copy();
|
|
48
|
+
if (reply_copy.has_field("error")) {
|
|
49
|
+
unowned GLib.Value? err_val = reply_copy.get_value("error");
|
|
50
|
+
if (err_val != null) {
|
|
51
|
+
GLib.Error err = (GLib.Error) err_val.get_boxed();
|
|
52
|
+
error_message = err != null ? err.message : "GstPromise error";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else if (result == Gst.PromiseResult.EXPIRED) {
|
|
57
|
+
error_message = "GstPromise expired";
|
|
58
|
+
} else if (result == Gst.PromiseResult.INTERRUPTED) {
|
|
59
|
+
error_message = "GstPromise interrupted";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
GLib.Idle.add(() => {
|
|
63
|
+
if (error_message != null) {
|
|
64
|
+
self.rejected(error_message);
|
|
65
|
+
} else {
|
|
66
|
+
self.replied(reply_copy);
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* ReceiverBridge — per-receiver incoming media pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Manages the GStreamer elements for one RTCRtpReceiver's incoming
|
|
5
|
+
* media path: muted source → tee (initial), then on connect_to_pad()
|
|
6
|
+
* wires webrtcbin output → decodebin → tee (replacing the muted source).
|
|
7
|
+
*
|
|
8
|
+
* All GStreamer signal handling (especially decodebin's pad-added which
|
|
9
|
+
* fires on the streaming thread) happens in C/Vala. JS only receives
|
|
10
|
+
* the main-thread-safe `media_flowing` signal when decoded media is
|
|
11
|
+
* actively flowing through the tee.
|
|
12
|
+
*
|
|
13
|
+
* Usage from JS:
|
|
14
|
+
* const bridge = new GjsifyWebrtc.ReceiverBridge({ pipeline, kind: 'audio' });
|
|
15
|
+
* bridge.connect_to_pad(webrtcbinSrcPad);
|
|
16
|
+
* bridge.connect('media-flowing', () => { track._setMuted(false); });
|
|
17
|
+
*/
|
|
18
|
+
namespace GjsifyWebrtc {
|
|
19
|
+
|
|
20
|
+
public class ReceiverBridge : GLib.Object {
|
|
21
|
+
|
|
22
|
+
public Gst.Pipeline pipeline { get; construct; }
|
|
23
|
+
public string kind { get; construct; }
|
|
24
|
+
|
|
25
|
+
/** Emitted on main thread when decoded media replaces the muted source. */
|
|
26
|
+
public signal void media_flowing();
|
|
27
|
+
|
|
28
|
+
private Gst.Element? _muted = null;
|
|
29
|
+
private Gst.Element _tee;
|
|
30
|
+
private Gst.Element? _decodebin = null;
|
|
31
|
+
private bool _switched = false;
|
|
32
|
+
private static int _counter = 0;
|
|
33
|
+
|
|
34
|
+
public ReceiverBridge(Gst.Pipeline pipeline, string kind) {
|
|
35
|
+
Object(pipeline: pipeline, kind: kind);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
construct {
|
|
39
|
+
int id = _counter++;
|
|
40
|
+
|
|
41
|
+
// Create muted source (silence for audio, black for video)
|
|
42
|
+
if (kind == "audio") {
|
|
43
|
+
_muted = Gst.ElementFactory.make("audiotestsrc", "recv-muted-audio-%d".printf(id));
|
|
44
|
+
_muted.set_property("wave", 4); // silence
|
|
45
|
+
_muted.set_property("is-live", true);
|
|
46
|
+
} else {
|
|
47
|
+
_muted = Gst.ElementFactory.make("videotestsrc", "recv-muted-video-%d".printf(id));
|
|
48
|
+
_muted.set_property("pattern", 2); // black
|
|
49
|
+
_muted.set_property("is-live", true);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Create tee for fan-out to track consumers
|
|
53
|
+
_tee = Gst.ElementFactory.make("tee", "recv-tee-%d".printf(id));
|
|
54
|
+
_tee.set_property("allow-not-linked", true);
|
|
55
|
+
|
|
56
|
+
// Add to pipeline and link: muted → tee
|
|
57
|
+
pipeline.add(_muted);
|
|
58
|
+
pipeline.add(_tee);
|
|
59
|
+
|
|
60
|
+
var muted_src = _muted.get_static_pad("src");
|
|
61
|
+
var tee_sink = _tee.get_static_pad("sink");
|
|
62
|
+
muted_src.link(tee_sink);
|
|
63
|
+
|
|
64
|
+
_muted.sync_state_with_parent();
|
|
65
|
+
_tee.sync_state_with_parent();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Wire the webrtcbin output pad through decodebin into the tee.
|
|
70
|
+
* Must be called from the main thread (JS calls this from
|
|
71
|
+
* _handlePadAdded via the WebrtcbinBridge signal).
|
|
72
|
+
*/
|
|
73
|
+
public void connect_to_pad(Gst.Pad src_pad) {
|
|
74
|
+
if (_decodebin != null) return; // already connected
|
|
75
|
+
|
|
76
|
+
int id = _counter++;
|
|
77
|
+
_decodebin = Gst.ElementFactory.make("decodebin", "recv-decodebin-%d".printf(id));
|
|
78
|
+
pipeline.add(_decodebin);
|
|
79
|
+
|
|
80
|
+
// Link webrtcbin src pad → decodebin sink
|
|
81
|
+
var dec_sink = _decodebin.get_static_pad("sink");
|
|
82
|
+
src_pad.link(dec_sink);
|
|
83
|
+
|
|
84
|
+
// Connect decodebin's pad-added signal — this fires on the
|
|
85
|
+
// streaming thread, which is why this entire class exists
|
|
86
|
+
_decodebin.pad_added.connect(this.on_decode_pad_added);
|
|
87
|
+
|
|
88
|
+
_decodebin.sync_state_with_parent();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Called on the STREAMING THREAD when decodebin has decoded the
|
|
93
|
+
* incoming RTP stream and created a src pad with raw media.
|
|
94
|
+
*/
|
|
95
|
+
private void on_decode_pad_added(Gst.Element element, Gst.Pad new_pad) {
|
|
96
|
+
// Only process src pads (decoded output)
|
|
97
|
+
if (new_pad.direction != Gst.PadDirection.SRC) return;
|
|
98
|
+
|
|
99
|
+
// Guard: only switch once (decodebin may fire multiple pad-added)
|
|
100
|
+
if (_switched) return;
|
|
101
|
+
_switched = true;
|
|
102
|
+
|
|
103
|
+
// Switch tee input from muted source to decoded pad
|
|
104
|
+
var tee_sink = _tee.get_static_pad("sink");
|
|
105
|
+
var old_peer = tee_sink.get_peer();
|
|
106
|
+
|
|
107
|
+
if (old_peer != null) {
|
|
108
|
+
// Add DROP probe on old pad to prevent errors during unlink
|
|
109
|
+
old_peer.add_probe(
|
|
110
|
+
Gst.PadProbeType.BLOCK | Gst.PadProbeType.DATA_DOWNSTREAM,
|
|
111
|
+
() => { return Gst.PadProbeReturn.DROP; }
|
|
112
|
+
);
|
|
113
|
+
old_peer.unlink(tee_sink);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Link decoded pad to tee
|
|
117
|
+
new_pad.link(tee_sink);
|
|
118
|
+
|
|
119
|
+
// Stop and remove muted element
|
|
120
|
+
if (_muted != null) {
|
|
121
|
+
_muted.set_state(Gst.State.NULL);
|
|
122
|
+
pipeline.remove(_muted);
|
|
123
|
+
_muted = null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Disconnect from decodebin pad-added (no longer needed)
|
|
127
|
+
_decodebin.pad_added.disconnect(this.on_decode_pad_added);
|
|
128
|
+
|
|
129
|
+
// Notify JS on main thread
|
|
130
|
+
GLib.Idle.add(() => {
|
|
131
|
+
this.media_flowing();
|
|
132
|
+
return false;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Request a src pad from the tee for a track consumer. */
|
|
137
|
+
public Gst.Pad request_src_pad() {
|
|
138
|
+
return _tee.request_pad_simple("src_%u");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Release a previously requested src pad. */
|
|
142
|
+
public void release_src_pad(Gst.Pad pad) {
|
|
143
|
+
_tee.release_request_pad(pad);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Clean up all GStreamer elements. Call from RTCRtpReceiver._dispose(). */
|
|
147
|
+
public void dispose_bridge() {
|
|
148
|
+
if (_decodebin != null) {
|
|
149
|
+
_decodebin.set_state(Gst.State.NULL);
|
|
150
|
+
pipeline.remove(_decodebin);
|
|
151
|
+
_decodebin = null;
|
|
152
|
+
}
|
|
153
|
+
if (_muted != null) {
|
|
154
|
+
_muted.set_state(Gst.State.NULL);
|
|
155
|
+
pipeline.remove(_muted);
|
|
156
|
+
_muted = null;
|
|
157
|
+
}
|
|
158
|
+
_tee.set_state(Gst.State.NULL);
|
|
159
|
+
pipeline.remove(_tee);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WebrtcbinBridge — webrtcbin signals → main-thread signals.
|
|
3
|
+
*
|
|
4
|
+
* webrtcbin emits on-negotiation-needed / on-ice-candidate /
|
|
5
|
+
* on-data-channel plus notify::*-state from GStreamer's internal
|
|
6
|
+
* streaming thread. GJS blocks JS callbacks on non-main threads, so
|
|
7
|
+
* the bridge connects on the C (Vala) side, captures arguments, hops
|
|
8
|
+
* to the GLib main context via GLib.Idle.add(), and only then emits
|
|
9
|
+
* its own signals which JS can safely consume.
|
|
10
|
+
*
|
|
11
|
+
* Usage from JS:
|
|
12
|
+
* const bridge = new GjsifyWebrtc.WebrtcbinBridge(webrtcbin);
|
|
13
|
+
* bridge.connect('icecandidate', (_b, mline, cand) => { ... });
|
|
14
|
+
* bridge.connect('datachannel', (_b, channel) => { ... });
|
|
15
|
+
*/
|
|
16
|
+
namespace GjsifyWebrtc {
|
|
17
|
+
|
|
18
|
+
public class WebrtcbinBridge : GLib.Object {
|
|
19
|
+
|
|
20
|
+
public Gst.Element bin { get; construct; }
|
|
21
|
+
|
|
22
|
+
/** on-negotiation-needed */
|
|
23
|
+
public signal void negotiation_needed();
|
|
24
|
+
/** on-ice-candidate(uint sdp_mline_index, string candidate) */
|
|
25
|
+
public signal void icecandidate(uint sdp_mline_index, string candidate);
|
|
26
|
+
/**
|
|
27
|
+
* on-data-channel — fired with a pre-wrapped DataChannelBridge so
|
|
28
|
+
* the wrapper is installed (and thus its signal handlers are active)
|
|
29
|
+
* *before* any on-message-* callbacks can fire on the streaming
|
|
30
|
+
* thread. Without this eager wrap, the first few messages from the
|
|
31
|
+
* remote peer would race the JS-side setup and get dropped.
|
|
32
|
+
*/
|
|
33
|
+
public signal void datachannel(DataChannelBridge channel_bridge);
|
|
34
|
+
/** on-new-transceiver(GstWebRTCRTPTransceiver) */
|
|
35
|
+
public signal void new_transceiver(Gst.WebRTCRTPTransceiver transceiver);
|
|
36
|
+
/** pad-added(Gst.Pad) — incoming RTP src pads from webrtcbin */
|
|
37
|
+
public signal void pad_added(Gst.Pad pad);
|
|
38
|
+
/** notify::connection-state */
|
|
39
|
+
public signal void connection_state_changed();
|
|
40
|
+
/** notify::signaling-state */
|
|
41
|
+
public signal void signaling_state_changed();
|
|
42
|
+
/** notify::ice-connection-state */
|
|
43
|
+
public signal void ice_connection_state_changed();
|
|
44
|
+
/** notify::ice-gathering-state */
|
|
45
|
+
public signal void ice_gathering_state_changed();
|
|
46
|
+
|
|
47
|
+
private ulong[] _handler_ids = {};
|
|
48
|
+
|
|
49
|
+
public WebrtcbinBridge(Gst.Element bin) {
|
|
50
|
+
Object(bin: bin);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
construct {
|
|
54
|
+
_handler_ids += GLib.Signal.connect(bin, "on-negotiation-needed",
|
|
55
|
+
(GLib.Callback) on_negotiation_needed_cb, this);
|
|
56
|
+
_handler_ids += GLib.Signal.connect(bin, "on-ice-candidate",
|
|
57
|
+
(GLib.Callback) on_ice_candidate_cb, this);
|
|
58
|
+
_handler_ids += GLib.Signal.connect(bin, "on-data-channel",
|
|
59
|
+
(GLib.Callback) on_data_channel_cb, this);
|
|
60
|
+
_handler_ids += GLib.Signal.connect(bin, "on-new-transceiver",
|
|
61
|
+
(GLib.Callback) on_new_transceiver_cb, this);
|
|
62
|
+
_handler_ids += GLib.Signal.connect(bin, "pad-added",
|
|
63
|
+
(GLib.Callback) on_pad_added_cb, this);
|
|
64
|
+
_handler_ids += bin.notify["connection-state"].connect(this.handle_connection_state);
|
|
65
|
+
_handler_ids += bin.notify["signaling-state"].connect(this.handle_signaling_state);
|
|
66
|
+
_handler_ids += bin.notify["ice-connection-state"].connect(this.handle_ice_connection_state);
|
|
67
|
+
_handler_ids += bin.notify["ice-gathering-state"].connect(this.handle_ice_gathering_state);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---- C-ABI callbacks for dynamic (non-VAPI) webrtcbin signals ----
|
|
71
|
+
// Signatures must match the GObject signal marshallers exactly
|
|
72
|
+
// (element as first arg, signal args next, user_data last).
|
|
73
|
+
|
|
74
|
+
[CCode (instance_pos = -1)]
|
|
75
|
+
private static void on_negotiation_needed_cb(Gst.Element element,
|
|
76
|
+
WebrtcbinBridge self) {
|
|
77
|
+
GLib.Idle.add(() => {
|
|
78
|
+
self.negotiation_needed();
|
|
79
|
+
return false;
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
[CCode (instance_pos = -1)]
|
|
84
|
+
private static void on_ice_candidate_cb(Gst.Element element,
|
|
85
|
+
uint mline_index,
|
|
86
|
+
string candidate,
|
|
87
|
+
WebrtcbinBridge self) {
|
|
88
|
+
uint idx = mline_index;
|
|
89
|
+
string cand = candidate;
|
|
90
|
+
GLib.Idle.add(() => {
|
|
91
|
+
self.icecandidate(idx, cand);
|
|
92
|
+
return false;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
[CCode (instance_pos = -1)]
|
|
97
|
+
private static void on_data_channel_cb(Gst.Element element,
|
|
98
|
+
Gst.WebRTCDataChannel channel,
|
|
99
|
+
WebrtcbinBridge self) {
|
|
100
|
+
// Eagerly wrap the incoming channel in a DataChannelBridge *on
|
|
101
|
+
// the streaming thread* so its signal handlers are installed
|
|
102
|
+
// before any on-message-* callbacks can fire. This avoids a
|
|
103
|
+
// race where early messages would arrive before the JS-side
|
|
104
|
+
// RTCDataChannel (and thus the DataChannelBridge) is created.
|
|
105
|
+
var bridge = new DataChannelBridge(channel);
|
|
106
|
+
GLib.Idle.add(() => {
|
|
107
|
+
self.datachannel(bridge);
|
|
108
|
+
return false;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
[CCode (instance_pos = -1)]
|
|
113
|
+
private static void on_new_transceiver_cb(Gst.Element element,
|
|
114
|
+
Gst.WebRTCRTPTransceiver trans,
|
|
115
|
+
WebrtcbinBridge self) {
|
|
116
|
+
var t = trans;
|
|
117
|
+
GLib.Idle.add(() => {
|
|
118
|
+
self.new_transceiver(t);
|
|
119
|
+
return false;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
[CCode (instance_pos = -1)]
|
|
124
|
+
private static void on_pad_added_cb(Gst.Element element,
|
|
125
|
+
Gst.Pad pad,
|
|
126
|
+
WebrtcbinBridge self) {
|
|
127
|
+
var p = pad;
|
|
128
|
+
GLib.Idle.add(() => {
|
|
129
|
+
self.pad_added(p);
|
|
130
|
+
return false;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---- notify:: handlers for state-change properties ----
|
|
135
|
+
|
|
136
|
+
private void handle_connection_state(GLib.ParamSpec pspec) {
|
|
137
|
+
GLib.Idle.add(() => {
|
|
138
|
+
this.connection_state_changed();
|
|
139
|
+
return false;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private void handle_signaling_state(GLib.ParamSpec pspec) {
|
|
144
|
+
GLib.Idle.add(() => {
|
|
145
|
+
this.signaling_state_changed();
|
|
146
|
+
return false;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private void handle_ice_connection_state(GLib.ParamSpec pspec) {
|
|
151
|
+
GLib.Idle.add(() => {
|
|
152
|
+
this.ice_connection_state_changed();
|
|
153
|
+
return false;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private void handle_ice_gathering_state(GLib.ParamSpec pspec) {
|
|
158
|
+
GLib.Idle.add(() => {
|
|
159
|
+
this.ice_gathering_state_changed();
|
|
160
|
+
return false;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Disconnect all handlers from the wrapped webrtcbin. Call this
|
|
166
|
+
* from RTCPeerConnection.close() to break the reference cycle
|
|
167
|
+
* before the pipeline is torn down.
|
|
168
|
+
*/
|
|
169
|
+
public void dispose_bridge() {
|
|
170
|
+
foreach (ulong id in _handler_ids) {
|
|
171
|
+
if (GLib.SignalHandler.is_connected(bin, id)) {
|
|
172
|
+
GLib.SignalHandler.disconnect(bin, id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
_handler_ids = {};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|