remoterigs-raspberrypi 0.1.0

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.
@@ -0,0 +1,483 @@
1
+ import { spawn } from 'child_process';
2
+ import axios from 'axios';
3
+ import { LogSettingType, VideoType, WebRTCMessage } from '../models/models.js';
4
+ import SignalR from './signalr.js';
5
+ import Debug from './debug.js';
6
+ export default class WebRTCStreamerWrapper {
7
+ constructor(component, clientRTCConfigurationViewModel, videoType) {
8
+ this.component = component;
9
+ this.clientRTCConfigurationViewModel = clientRTCConfigurationViewModel;
10
+ this.videoType = videoType;
11
+ this.webrtcStreamerProcess = null;
12
+ this.streamerStarted = false;
13
+ // ICE candidate polling
14
+ this.iceCandidatePollers = new Map();
15
+ this.sentCandidates = new Map(); // Track verzonden candidates per peer
16
+ this.ICE_POLLING_INTERVAL = 1000; // Verlaagd naar 1 seconde (was 100ms)
17
+ this.MAX_POLL_ATTEMPTS = 30; // Stop na 30 seconden
18
+ this.STREAMER_PORT = 8000;
19
+ this.STREAMER_BASE_URL = `http://0.0.0.0:${this.STREAMER_PORT}`;
20
+ this.candidateBuffer = new Map();
21
+ this.remoteDescriptionSet = new Set();
22
+ this.streamerClient = axios.create({
23
+ baseURL: this.STREAMER_BASE_URL,
24
+ timeout: 5000,
25
+ headers: {
26
+ 'Content-Type': 'application/json'
27
+ }
28
+ });
29
+ }
30
+ async Start() {
31
+ this.component.SetStatusState("State", "Starting");
32
+ this.Log("Starting webrtc-streamer...");
33
+ await this.StartWebRTCStreamer();
34
+ await this.WaitForStreamer();
35
+ this.Log("webrtc-streamer is ready");
36
+ }
37
+ async Stop() {
38
+ // Stop alle ICE polling
39
+ for (const [peerId, _] of this.iceCandidatePollers) {
40
+ this.StopIceCandidatePolling(peerId);
41
+ }
42
+ // Stop webrtc-streamer process
43
+ if (this.webrtcStreamerProcess) {
44
+ this.webrtcStreamerProcess.kill();
45
+ this.webrtcStreamerProcess = null;
46
+ }
47
+ }
48
+ async PutWebRTCMessage(message) {
49
+ if (this.streamerStarted) {
50
+ this.Log("PutWebRTCMessage username: " + message.username + " messageType: " + message.messageType);
51
+ if (message.messageType == "getIceServers") {
52
+ await this.GetIceServers(message);
53
+ }
54
+ else if (message.messageType == "call") {
55
+ await this.Call(message);
56
+ }
57
+ else if (message.messageType == "createOffer") {
58
+ await this.CreateOffer(message);
59
+ }
60
+ else if (message.messageType == "setAnswer") {
61
+ await this.SetAnswer(message);
62
+ this.remoteDescriptionSet.add(message.sessionId);
63
+ await this.FlushBufferedCandidates(message.sessionId);
64
+ }
65
+ else if (message.messageType == "onReceiveCall") {
66
+ // SDP answer ontvangen, markeer als 'set' en flush buffer
67
+ this.remoteDescriptionSet.add(message.sessionId);
68
+ await this.FlushBufferedCandidates(message.sessionId);
69
+ }
70
+ else if (message.messageType == "addIceCandidate") {
71
+ // Buffer als SDP answer nog niet ontvangen
72
+ if (!this.remoteDescriptionSet.has(message.sessionId)) {
73
+ this.Log(`Buffering ICE candidate for session ${message.sessionId}`);
74
+ if (!this.candidateBuffer.has(message.sessionId)) {
75
+ this.candidateBuffer.set(message.sessionId, []);
76
+ }
77
+ this.candidateBuffer.get(message.sessionId).push(message.message);
78
+ }
79
+ else {
80
+ await this.AddIceCandidate(message);
81
+ }
82
+ }
83
+ else if (message.messageType == "hangup") {
84
+ await this.Hangup(message);
85
+ // Opruimen na hangup
86
+ this.remoteDescriptionSet.delete(message.sessionId);
87
+ this.candidateBuffer.delete(message.sessionId);
88
+ }
89
+ }
90
+ else {
91
+ this.Log("IGNORED! PutWebRTCMessage username: " + message.username + " messageType: " + message.messageType);
92
+ }
93
+ }
94
+ async StartWebRTCStreamer() {
95
+ try {
96
+ const response = await this.streamerClient.get('/api/version');
97
+ this.Log("webrtc-streamer is already running: " + JSON.stringify(response));
98
+ return;
99
+ }
100
+ catch (err) {
101
+ this.Log("webrtc-streamer not running, starting it...");
102
+ }
103
+ const args = [
104
+ "-H", `0.0.0.0:${this.STREAMER_PORT}`,
105
+ "-s",
106
+ //"--stun", this.clientRTCConfigurationViewModel.stunServer,
107
+ "--turn", this.clientRTCConfigurationViewModel.turnServer
108
+ ];
109
+ const command = "webrtc-streamer";
110
+ this.webrtcStreamerProcess = spawn(command, args);
111
+ if (this.webrtcStreamerProcess != null) {
112
+ if (this.webrtcStreamerProcess.stdout != null) {
113
+ this.webrtcStreamerProcess.stdout.on("data", (data) => {
114
+ this.Log(`webrtc-streamer stdout: ${data}`);
115
+ });
116
+ }
117
+ if (this.webrtcStreamerProcess.stderr != null) {
118
+ this.webrtcStreamerProcess.stderr.on("data", (data) => {
119
+ this.Log(`webrtc-streamer stderr: ${data}`);
120
+ });
121
+ }
122
+ this.webrtcStreamerProcess.on("error", (err) => {
123
+ this.Error(`webrtc-streamer error: ${JSON.stringify(err)}`);
124
+ });
125
+ this.webrtcStreamerProcess.on("close", (code) => {
126
+ this.Log(`webrtc-streamer exited with code ${code}`);
127
+ });
128
+ }
129
+ this.Log("Spawned webrtc-streamer process: " + command);
130
+ }
131
+ async WaitForStreamer() {
132
+ const maxRetries = 20;
133
+ const retryDelay = 500;
134
+ for (let i = 0; i < maxRetries; i++) {
135
+ var response = null;
136
+ var skip = false;
137
+ try {
138
+ response = await this.streamerClient.get('/api/version');
139
+ }
140
+ catch (err) {
141
+ //this.Log("WaitForStreamer err: " + err);
142
+ skip = true;
143
+ }
144
+ if (response?.data && !skip) {
145
+ this.streamerStarted = true;
146
+ this.component.SetStatusState("State", "Started");
147
+ this.Log("WaitForStreamer version: " + response.data);
148
+ //const getMediaList = await this.streamerClient.get('/api/getMediaList');
149
+ //this.Log("WaitForStreamer getMediaList: " + JSON.stringify(getMediaList.data, null, 2));
150
+ const getVideoDeviceList = await this.streamerClient.get('/api/getVideoDeviceList');
151
+ this.Log("WaitForStreamer getVideoDeviceList: " + getVideoDeviceList.data);
152
+ const getAudioDeviceList = await this.streamerClient.get('/api/getAudioDeviceList');
153
+ this.Log("WaitForStreamer getAudioDeviceList: " + getAudioDeviceList.data);
154
+ //const getAudioPlayoutList = await this.streamerClient.get('/api/getAudioPlayoutList');
155
+ //this.Log("WaitForStreamer getAudioPlayoutList: " + getAudioPlayoutList.data);
156
+ //const getPeerConnectionList = await this.streamerClient.get('/api/getPeerConnectionList');
157
+ //this.Log("WaitForStreamer getPeerConnectionList: " + getPeerConnectionList.data);
158
+ //const getStreamList = await this.streamerClient.get('/api/getStreamList');
159
+ //this.Log("WaitForStreamer getStreamList: " + getStreamList.data);
160
+ return;
161
+ }
162
+ this.Log("WaitForStreamer starting wait: " + i + "/" + maxRetries + " - " + retryDelay + "ms");
163
+ var start = new Date().getTime();
164
+ var end = start;
165
+ while (end < start + retryDelay) {
166
+ end = new Date().getTime();
167
+ }
168
+ }
169
+ if (this.streamerStarted == false) {
170
+ this.component.SetStatusState("State", "Stopped");
171
+ throw new Error("webrtc-streamer failed to start");
172
+ }
173
+ }
174
+ async FlushBufferedCandidates(sessionId) {
175
+ const buffer = this.candidateBuffer.get(sessionId);
176
+ if (buffer && buffer.length > 0) {
177
+ this.Log(`Flushing ${buffer.length} buffered ICE candidates for session ${sessionId}`);
178
+ this.candidateBuffer.delete(sessionId);
179
+ }
180
+ }
181
+ async GetIceServers(message) {
182
+ try {
183
+ this.Log("Request Ice servers /api/getIceServers");
184
+ const response = await this.streamerClient.get('/api/getIceServers');
185
+ this.Log("Response status: " + response.status);
186
+ this.Log("Response data: " + JSON.stringify(response.data));
187
+ var webrtcMessage = new WebRTCMessage();
188
+ webrtcMessage.componentId = this.component.component.id;
189
+ webrtcMessage.rigId = message.rigId;
190
+ webrtcMessage.webRTCConnectionType = message.webRTCConnectionType;
191
+ webrtcMessage.sessionId = message.sessionId;
192
+ webrtcMessage.username = message.username;
193
+ webrtcMessage.rigOwnerUsername = message.rigOwnerUsername;
194
+ webrtcMessage.messageType = 'getIceServersResponse';
195
+ webrtcMessage.message = JSON.stringify(response.data);
196
+ SignalR.SendWebRTCMessage(webrtcMessage);
197
+ }
198
+ catch (err) {
199
+ if (err.response) {
200
+ this.Log("Response status: " + err.response.status);
201
+ this.Log("Response data: " + JSON.stringify(err.response.data));
202
+ }
203
+ this.Error("GetIceServers error: " + err.message);
204
+ }
205
+ }
206
+ async Call(message) {
207
+ try {
208
+ var devicePath = this.component.GetProperty("devicepath");
209
+ if (devicePath != null && devicePath != "") {
210
+ var audioPath = this.component.GetProperty("audiopath");
211
+ var audioStr = "";
212
+ if (audioPath != null && audioPath != "") {
213
+ audioStr = "&audiourl=" + encodeURIComponent(audioPath);
214
+ }
215
+ var width = this.component.GetProperty("width");
216
+ var widthStr = "";
217
+ if (width != null && width != "") {
218
+ widthStr = "&width=" + width;
219
+ }
220
+ var height = this.component.GetProperty("height");
221
+ var heightStr = "";
222
+ if (height != null && height != "") {
223
+ heightStr = "&height=" + height;
224
+ }
225
+ var bitrate = this.component.GetProperty("bitrate");
226
+ var bitrateStr = "";
227
+ if (bitrate != null && bitrate != "") {
228
+ bitrateStr = "&bitrate=" + bitrate;
229
+ }
230
+ //let options = encodeURIComponent("rtptransport=tcp&width=1280&height=720&fps=30&bitrate=2000000");
231
+ let callurl = "/api/call?peerid=" + message.sessionId +
232
+ "&url=" + encodeURIComponent(devicePath) +
233
+ audioStr +
234
+ "&options=" + encodeURIComponent("rtptransport=tcp" + widthStr + heightStr + bitrateStr);
235
+ //m_func[basePath + "/api/call"] = [this](const struct mg_request_info * req_info, const Json:: Value &in) -> HttpServerRequestHandler::httpFunctionReturn {
236
+ // std::string peerid = getParam(req_info -> query_string, "peerid");
237
+ // std::string url = getParam(req_info -> query_string, "url");
238
+ // std::string audiourl = getParam(req_info -> query_string, "audiourl");
239
+ // std::string options = getParam(req_info -> query_string, "options");
240
+ // return std:: make_tuple(200, std:: map < std:: string, std:: string > (), this -> call(peerid, url, audiourl, options, in));
241
+ //};
242
+ this.Log("Call: " + callurl);
243
+ const response = await this.streamerClient.post(callurl, message.message);
244
+ this.Log("Response status: " + response.status);
245
+ this.Log("Response data: " + JSON.stringify(response.data));
246
+ // Stuur de answer terug naar de client
247
+ var webrtcMessage = new WebRTCMessage();
248
+ webrtcMessage.componentId = this.component.component.id;
249
+ webrtcMessage.rigId = message.rigId;
250
+ webrtcMessage.sessionId = message.sessionId;
251
+ webrtcMessage.webRTCConnectionType = message.webRTCConnectionType;
252
+ webrtcMessage.username = message.username;
253
+ webrtcMessage.rigOwnerUsername = message.rigOwnerUsername;
254
+ webrtcMessage.messageType = 'onReceiveCall';
255
+ webrtcMessage.message = JSON.stringify(response.data);
256
+ SignalR.SendWebRTCMessage(webrtcMessage);
257
+ // Start polling voor ICE candidates van de server
258
+ this.StartIceCandidatePolling(message.sessionId, message.rigId, message.username, message.rigOwnerUsername, message.webRTCConnectionType);
259
+ }
260
+ else {
261
+ this.Error("devicePath is empty");
262
+ }
263
+ }
264
+ catch (err) {
265
+ if (err.response) {
266
+ this.Log("Response status: " + err.response.status);
267
+ this.Log("Response data: " + JSON.stringify(err.response.data));
268
+ }
269
+ this.Error("Call error: " + err.message);
270
+ }
271
+ }
272
+ async CreateOffer(message) {
273
+ //m_func[basePath + "/api/createOffer"] = [this](const struct mg_request_info * req_info, const Json:: Value &in) -> HttpServerRequestHandler::httpFunctionReturn {
274
+ // std::string peerid = getParam(req_info -> query_string, "peerid");
275
+ // std::string url = getParam(req_info -> query_string, "url");
276
+ // std::string audiourl = getParam(req_info -> query_string, "audiourl");
277
+ // std::string options = getParam(req_info -> query_string, "options");
278
+ // return std:: make_tuple(200, std:: map < std:: string, std:: string > (), this -> createOffer(peerid, url, audiourl, options));
279
+ //};
280
+ try {
281
+ var devicePath = this.component.GetProperty("devicepath");
282
+ if (devicePath != null && devicePath != "") {
283
+ var audioPath = this.component.GetProperty("audiopath");
284
+ var audioStr = "";
285
+ if (audioPath != null && audioPath != "") {
286
+ audioStr = "&audiourl=" + encodeURIComponent(audioPath);
287
+ }
288
+ var width = this.component.GetProperty("width");
289
+ var widthStr = "";
290
+ if (width != null && width != "") {
291
+ widthStr = "&width=" + width;
292
+ }
293
+ var height = this.component.GetProperty("height");
294
+ var heightStr = "";
295
+ if (height != null && height != "") {
296
+ heightStr = "&height=" + height;
297
+ }
298
+ var bitrate = this.component.GetProperty("bitrate");
299
+ var bitrateStr = "";
300
+ if (bitrate != null && bitrate != "") {
301
+ bitrateStr = "&bitrate=" + bitrate;
302
+ }
303
+ let createOfferurl = "/api/createOffer?peerid=" + message.sessionId +
304
+ "&url=" + encodeURIComponent(devicePath) +
305
+ audioStr +
306
+ "&options=" + encodeURIComponent("rtptransport=tcp" + widthStr + heightStr + bitrateStr);
307
+ this.Log("createOffer: " + createOfferurl);
308
+ const response = await this.streamerClient.post(createOfferurl, message.message);
309
+ let sdp = response.data.sdp;
310
+ if (this.videoType == VideoType.H264) {
311
+ // Zet H264 (100+101) als eerste in de m=video regel
312
+ sdp = sdp.replace(/m=video (\S+) (\S+) ([\d ]+)/, (_match, port, proto, payloads) => {
313
+ const ids = payloads.trim().split(' ');
314
+ const preferred = ['100', '101'];
315
+ const rest = ids.filter((id) => !preferred.includes(id));
316
+ return `m=video ${port} ${proto} ${[...preferred, ...rest].join(' ')}`;
317
+ });
318
+ }
319
+ this.Log("CreateOffer Response status: " + response.status);
320
+ this.Log("CreateOffer Response SDP:\\r\\n" + JSON.stringify(sdp));
321
+ // Stuur de answer terug naar de client
322
+ var webrtcMessage = new WebRTCMessage();
323
+ webrtcMessage.componentId = this.component.component.id;
324
+ webrtcMessage.rigId = message.rigId;
325
+ webrtcMessage.webRTCConnectionType = message.webRTCConnectionType;
326
+ webrtcMessage.sessionId = message.sessionId;
327
+ webrtcMessage.username = message.username;
328
+ webrtcMessage.rigOwnerUsername = message.rigOwnerUsername;
329
+ webrtcMessage.messageType = 'receiveOffer';
330
+ //webrtcMessage.message = JSON.stringify(response.data);
331
+ webrtcMessage.message = JSON.stringify({ ...response.data, sdp });
332
+ SignalR.SendWebRTCMessage(webrtcMessage);
333
+ }
334
+ else {
335
+ this.Error("devicePath is empty");
336
+ }
337
+ }
338
+ catch (err) {
339
+ if (err.response) {
340
+ this.Log("CreateOffer Response status: " + err.response.status);
341
+ this.Log("CreateOffer Response data: " + JSON.stringify(err.response.data));
342
+ }
343
+ this.Error("CreateOffer Call error: " + err.message);
344
+ }
345
+ }
346
+ async SetAnswer(message) {
347
+ //m_func[basePath + "/api/setAnswer"] = [this](const struct mg_request_info * req_info, const Json:: Value &in) -> HttpServerRequestHandler::httpFunctionReturn {
348
+ // std::string peerid = getParam(req_info -> query_string, "peerid");
349
+ // return std:: make_tuple(200, std:: map < std:: string, std:: string > (), this -> setAnswer(peerid, in));
350
+ //};
351
+ try {
352
+ let setAnswerUrl = "/api/setAnswer?peerid=" + message.sessionId;
353
+ this.Log("setAnswer: " + setAnswerUrl + " message: " + message.message);
354
+ var tJSON = JSON.parse(message.message);
355
+ const response = await this.streamerClient.post(setAnswerUrl, tJSON);
356
+ this.Log("SetAnswer Response status: " + response.status);
357
+ this.Log("SetAnswer Response data:\\r\\n" + JSON.stringify(response.data?.sdp));
358
+ this.StartIceCandidatePolling(message.sessionId, message.rigId, message.username, message.rigOwnerUsername, message.webRTCConnectionType);
359
+ }
360
+ catch (err) {
361
+ if (err.response) {
362
+ this.Log("SetAnswer Response status: " + err.response.status);
363
+ this.Log("SetAnswer Response data: " + JSON.stringify(err.response.data));
364
+ }
365
+ this.Error("SetAnswer Call error: " + err.message);
366
+ }
367
+ }
368
+ StartIceCandidatePolling(peerId, rigId, username, rigOwnerUsername, webRTCConnectionType) {
369
+ // Stop eventuele bestaande polling voor deze peer
370
+ this.StopIceCandidatePolling(peerId);
371
+ // Initialiseer candidate tracking voor deze peer
372
+ this.sentCandidates.set(peerId, new Set());
373
+ this.Log(`Starting ICE candidate polling for peer: ${peerId} with WebRTC connection type: ${webRTCConnectionType}`);
374
+ let pollAttempts = 0;
375
+ const poller = setInterval(async () => {
376
+ pollAttempts++;
377
+ // Stop na MAX_POLL_ATTEMPTS
378
+ if (pollAttempts > this.MAX_POLL_ATTEMPTS) {
379
+ this.Log(`Stopping ICE polling for peer ${peerId} after ${pollAttempts} attempts`);
380
+ this.StopIceCandidatePolling(peerId);
381
+ return;
382
+ }
383
+ try {
384
+ const url = '/api/getIceCandidate?peerid=' + peerId;
385
+ const response = await this.streamerClient.get(url);
386
+ // Als er ICE candidates zijn, filter en stuur ze door
387
+ if (response.data && Array.isArray(response.data) && response.data.length > 0) {
388
+ const sentSet = this.sentCandidates.get(peerId);
389
+ const newCandidates = response.data.filter(candidate => {
390
+ // Maak unieke ID voor deze candidate
391
+ const candidateId = this.GetCandidateId(candidate);
392
+ return !sentSet.has(candidateId);
393
+ });
394
+ if (newCandidates.length > 0) {
395
+ this.Log(`Got ${newCandidates.length} NEW ICE candidates for peer ${peerId} (filtered from ${response.data.length} total)`);
396
+ for (const candidate of newCandidates) {
397
+ const candidateId = this.GetCandidateId(candidate);
398
+ sentSet.add(candidateId);
399
+ const webrtcMessage = new WebRTCMessage();
400
+ webrtcMessage.componentId = this.component.component.id;
401
+ webrtcMessage.rigId = rigId;
402
+ webrtcMessage.sessionId = peerId;
403
+ webrtcMessage.webRTCConnectionType = webRTCConnectionType;
404
+ webrtcMessage.username = username;
405
+ webrtcMessage.rigOwnerUsername = rigOwnerUsername;
406
+ webrtcMessage.messageType = 'onReceiveCandidate';
407
+ webrtcMessage.message = JSON.stringify(candidate);
408
+ SignalR.SendWebRTCMessage(webrtcMessage);
409
+ }
410
+ }
411
+ }
412
+ }
413
+ catch (err) {
414
+ // Negeer 404 fouten (geen candidates beschikbaar)
415
+ if (err.response?.status !== 404) {
416
+ this.Log(`ICE polling error for ${peerId}: ${err.message}`);
417
+ }
418
+ }
419
+ }, this.ICE_POLLING_INTERVAL);
420
+ this.iceCandidatePollers.set(peerId, poller);
421
+ }
422
+ GetCandidateId(candidate) {
423
+ // Maak een unieke ID voor deze candidate gebaseerd op de kandidaat string
424
+ // Dit voorkomt dat we dezelfde candidate meerdere keren versturen
425
+ if (candidate && candidate.candidate) {
426
+ return candidate.candidate;
427
+ }
428
+ return JSON.stringify(candidate);
429
+ }
430
+ StopIceCandidatePolling(peerId) {
431
+ const poller = this.iceCandidatePollers.get(peerId);
432
+ if (poller) {
433
+ clearInterval(poller);
434
+ this.iceCandidatePollers.delete(peerId);
435
+ this.sentCandidates.delete(peerId);
436
+ this.Log(`Stopped ICE candidate polling for peer: ${peerId}`);
437
+ }
438
+ }
439
+ async AddIceCandidate(message) {
440
+ try {
441
+ var url = "/api/addIceCandidate?peerid=" + message.sessionId;
442
+ var messageObj = JSON.parse(message.message);
443
+ if (messageObj.candidate != null && messageObj.candidate != "") {
444
+ this.Log("AddIceCandidate: " + url + " - " + messageObj.candidate);
445
+ await this.streamerClient.post(url, message.message);
446
+ this.Log("ICE candidate added successfully");
447
+ }
448
+ else {
449
+ this.Log("AddIceCandidate: ignoring empty candidate");
450
+ }
451
+ }
452
+ catch (err) {
453
+ if (err.response) {
454
+ this.Log("AddIceCandidate error Response status: " + err.response.status + " - data: " + JSON.stringify(err.response.data));
455
+ }
456
+ this.Log("AddIceCandidate error: " + err.message);
457
+ this.Log("AddIceCandidate message: " + JSON.stringify(message));
458
+ }
459
+ }
460
+ async Hangup(message) {
461
+ try {
462
+ const url = '/api/hangup?peerid=' + message.sessionId;
463
+ this.Log("Hangup: " + url);
464
+ // Stop ICE polling voor deze peer
465
+ this.StopIceCandidatePolling(message.sessionId);
466
+ await this.streamerClient.get(url);
467
+ this.Log("Hangup successful");
468
+ }
469
+ catch (err) {
470
+ this.Error("Hangup error: " + err.message);
471
+ }
472
+ }
473
+ GetName() {
474
+ return "#" + this.component.component.id + " WebcamStreamer";
475
+ }
476
+ Log(message) {
477
+ Debug.Log(LogSettingType.Component, this.component.component.id, this.GetName(), message);
478
+ }
479
+ Error(message) {
480
+ Debug.Error(this.GetName(), message);
481
+ }
482
+ }
483
+ //# sourceMappingURL=webrtc-streamer-wrapper.js.map