ajout app
This commit is contained in:
62
MiroTalk SFU/app/api/README.md
Normal file
62
MiroTalk SFU/app/api/README.md
Normal file
@ -0,0 +1,62 @@
|
||||

|
||||
|
||||
## Create a meeting
|
||||
|
||||
Create a meeting with a `HTTP request` containing the `API_KEY` sent to MiroTalk’s server. The response contains a `meeting` URL that can be `embedded` in your client within an `iframe`.
|
||||
|
||||
The `API_KEY` is defined in the `app/src/config.js`, change it with your own.
|
||||
|
||||
```js
|
||||
api: {
|
||||
// app/api
|
||||
keySecret: 'mirotalksfu_default_secret',
|
||||
}
|
||||
```
|
||||
|
||||
Some examples demonstrating how to call the API:
|
||||
|
||||
```bash
|
||||
# js
|
||||
node meetings.js
|
||||
node meeting.js
|
||||
node join.js
|
||||
|
||||
# php
|
||||
php meetings.php
|
||||
php meeting.php
|
||||
php join.php
|
||||
|
||||
# python
|
||||
python3 meetings.py
|
||||
python3 meeting.py
|
||||
python3 join.py
|
||||
|
||||
# bash
|
||||
./meetings.sh
|
||||
./meeting.sh
|
||||
./join.sh
|
||||
```
|
||||
|
||||
## Embed a meeting
|
||||
|
||||
Embedding a meeting into a `service` or `app` requires using an `iframe` with the `src` attribute specified as the `meeting` from `HTTP response`.
|
||||
|
||||
```html
|
||||
<iframe
|
||||
allow="camera; microphone; display-capture; fullscreen; clipboard-read; clipboard-write; autoplay"
|
||||
src="https://sfu.mirotalk.com/join/your_room_name"
|
||||
style="height: 100vh; width: 100vw; border: 0px;"
|
||||
></iframe>
|
||||
```
|
||||
|
||||
## Fast Integration
|
||||
|
||||
Develop your `website` or `application`, and bring `video meetings` in with a simple `iframe`.
|
||||
|
||||
```html
|
||||
<iframe
|
||||
allow="camera; microphone; display-capture; fullscreen; clipboard-read; clipboard-write; autoplay"
|
||||
src="https://sfu.mirotalk.com/newroom"
|
||||
style="height: 100vh; width: 100vw; border: 0px;"
|
||||
></iframe>
|
||||
```
|
46
MiroTalk SFU/app/api/join/join.js
Normal file
46
MiroTalk SFU/app/api/join/join.js
Normal file
@ -0,0 +1,46 @@
|
||||
'use strict';
|
||||
|
||||
async function getJoin() {
|
||||
try {
|
||||
// Use dynamic import with await
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
|
||||
const API_KEY_SECRET = 'mirotalksfu_default_secret';
|
||||
const MIROTALK_URL = 'https://sfu.mirotalk.com/api/v1/join';
|
||||
//const MIROTALK_URL = 'http://localhost:3010/api/v1/join';
|
||||
|
||||
const response = await fetch(MIROTALK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: API_KEY_SECRET,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
room: 'test',
|
||||
roomPassword: false,
|
||||
name: 'mirotalksfu',
|
||||
audio: true,
|
||||
video: true,
|
||||
screen: true,
|
||||
hide: false,
|
||||
notify: true,
|
||||
token: {
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
presenter: true,
|
||||
expire: '1h',
|
||||
},
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
console.log('Error:', data.error);
|
||||
} else {
|
||||
console.log('join:', data.join);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getJoin();
|
45
MiroTalk SFU/app/api/join/join.php
Normal file
45
MiroTalk SFU/app/api/join/join.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
$API_KEY_SECRET = "mirotalksfu_default_secret";
|
||||
$MIROTALK_URL = "https://sfu.mirotalk.com/api/v1/join";
|
||||
// $MIROTALK_URL = "http://localhost:3010/api/v1/join";
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $MIROTALK_URL);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
|
||||
$headers = [
|
||||
'authorization:' . $API_KEY_SECRET,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
$data = array(
|
||||
"room" => "test",
|
||||
"roomPassword" => false,
|
||||
"name" => "mirotalksfu",
|
||||
"audio" => true,
|
||||
"video" => true,
|
||||
"screen" => true,
|
||||
"hide" => false,
|
||||
"notify" => true,
|
||||
"token" => array(
|
||||
"username" => "username",
|
||||
"password" => "password",
|
||||
"presenter" => true,
|
||||
"expire" => "1h",
|
||||
),
|
||||
);
|
||||
$data_string = json_encode($data);
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
|
||||
$response = curl_exec($ch);
|
||||
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
echo "Status code: $httpcode \n";
|
||||
$data = json_decode($response);
|
||||
echo "join: ", $data->{'join'}, "\n";
|
39
MiroTalk SFU/app/api/join/join.py
Normal file
39
MiroTalk SFU/app/api/join/join.py
Normal file
@ -0,0 +1,39 @@
|
||||
# pip3 install requests
|
||||
import requests
|
||||
import json
|
||||
|
||||
API_KEY_SECRET = "mirotalksfu_default_secret"
|
||||
MIROTALK_URL = "https://sfu.mirotalk.com/api/v1/join"
|
||||
# MIROTALK_URL = "http://localhost:3010/api/v1/join"
|
||||
|
||||
headers = {
|
||||
"authorization": API_KEY_SECRET,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
data = {
|
||||
"room": "test",
|
||||
"roomPassword": "false",
|
||||
"name": "mirotalksfu",
|
||||
"audio": "true",
|
||||
"video": "true",
|
||||
"screen": "true",
|
||||
"hide": "false",
|
||||
"notify": "true",
|
||||
"token": {
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"presenter": "true",
|
||||
"expire": "1h",
|
||||
}
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
MIROTALK_URL,
|
||||
headers=headers,
|
||||
json=data,
|
||||
)
|
||||
|
||||
print("Status code:", response.status_code)
|
||||
data = json.loads(response.text)
|
||||
print("join:", data["join"])
|
11
MiroTalk SFU/app/api/join/join.sh
Normal file
11
MiroTalk SFU/app/api/join/join.sh
Normal file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
API_KEY_SECRET="mirotalksfu_default_secret"
|
||||
MIROTALK_URL="https://sfu.mirotalk.com/api/v1/join"
|
||||
# MIROTALK_URL="http://localhost:3010/api/v1/join"
|
||||
|
||||
curl $MIROTALK_URL \
|
||||
--header "authorization: $API_KEY_SECRET" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"room":"test","roomPassword":"false","name":"mirotalksfu","audio":"true","video":"true","screen":"false","hide":"false","notify":"true","token":{"username":"username","password":"password","presenter":"true", "expire":"1h"}}' \
|
||||
--request POST
|
30
MiroTalk SFU/app/api/meeting/meeting.js
Normal file
30
MiroTalk SFU/app/api/meeting/meeting.js
Normal file
@ -0,0 +1,30 @@
|
||||
'use strict';
|
||||
|
||||
async function getMeeting() {
|
||||
try {
|
||||
// Use dynamic import with await
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
|
||||
const API_KEY_SECRET = 'mirotalksfu_default_secret';
|
||||
const MIROTALK_URL = 'https://sfu.mirotalk.com/api/v1/meeting';
|
||||
// const MIROTALK_URL = 'http://localhost:3010/api/v1/meeting';
|
||||
|
||||
const response = await fetch(MIROTALK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: API_KEY_SECRET,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
console.log('Error:', data.error);
|
||||
} else {
|
||||
console.log('meeting:', data.meeting);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getMeeting();
|
25
MiroTalk SFU/app/api/meeting/meeting.php
Normal file
25
MiroTalk SFU/app/api/meeting/meeting.php
Normal file
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
$API_KEY_SECRET = "mirotalksfu_default_secret";
|
||||
$MIROTALK_URL = "https://sfu.mirotalk.com/api/v1/meeting";
|
||||
// $MIROTALK_URL = "http://localhost:3010/api/v1/meeting";
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $MIROTALK_URL);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
|
||||
$headers = [
|
||||
'authorization:' . $API_KEY_SECRET,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
$response = curl_exec($ch);
|
||||
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
echo "Status code: $httpcode \n";
|
||||
$data = json_decode($response);
|
||||
echo "meeting: ", $data->{'meeting'}, "\n";
|
21
MiroTalk SFU/app/api/meeting/meeting.py
Normal file
21
MiroTalk SFU/app/api/meeting/meeting.py
Normal file
@ -0,0 +1,21 @@
|
||||
# pip3 install requests
|
||||
import requests
|
||||
import json
|
||||
|
||||
API_KEY_SECRET = "mirotalksfu_default_secret"
|
||||
MIROTALK_URL = "https://sfu.mirotalk.com/api/v1/meeting"
|
||||
# MIROTALK_URL = "http://localhost:3010/api/v1/meeting"
|
||||
|
||||
headers = {
|
||||
"authorization": API_KEY_SECRET,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
MIROTALK_URL,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
print("Status code:", response.status_code)
|
||||
data = json.loads(response.text)
|
||||
print("meeting:", data["meeting"])
|
10
MiroTalk SFU/app/api/meeting/meeting.sh
Normal file
10
MiroTalk SFU/app/api/meeting/meeting.sh
Normal file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
API_KEY_SECRET="mirotalksfu_default_secret"
|
||||
MIROTALK_URL="https://sfu.mirotalk.com/api/v1/meeting"
|
||||
# MIROTALK_URL="http://localhost:3010/api/v1/meeting"
|
||||
|
||||
curl $MIROTALK_URL \
|
||||
--header "authorization: $API_KEY_SECRET" \
|
||||
--header "Content-Type: application/json" \
|
||||
--request POST
|
34
MiroTalk SFU/app/api/meetings/meetings.js
Normal file
34
MiroTalk SFU/app/api/meetings/meetings.js
Normal file
@ -0,0 +1,34 @@
|
||||
'use strict';
|
||||
|
||||
async function getMeetings() {
|
||||
try {
|
||||
// Use dynamic import with await
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
|
||||
const API_KEY_SECRET = 'mirotalksfu_default_secret';
|
||||
const MIROTALK_URL = 'https://sfu.mirotalk.com/api/v1/meetings';
|
||||
//const MIROTALK_URL = 'http://localhost:3010/api/v1/meetings';
|
||||
|
||||
const response = await fetch(MIROTALK_URL, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
authorization: API_KEY_SECRET,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
console.log('Error:', data.error);
|
||||
} else {
|
||||
if (data && data.meetings) {
|
||||
const meetings = data.meetings;
|
||||
const formattedData = JSON.stringify({ meetings }, null, 2);
|
||||
console.log(formattedData);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getMeetings();
|
29
MiroTalk SFU/app/api/meetings/meetings.php
Normal file
29
MiroTalk SFU/app/api/meetings/meetings.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
$API_KEY_SECRET = "mirotalksfu_default_secret";
|
||||
$MIROTALK_URL = "https://sfu.mirotalk.com/api/v1/meetings";
|
||||
//$MIROTALK_URL = "http://localhost:3010/api/v1/meetings";
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $MIROTALK_URL);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPGET, true);
|
||||
|
||||
$headers = [
|
||||
'authorization:' . $API_KEY_SECRET,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
$response = curl_exec($ch);
|
||||
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
echo "Status code: $httpcode \n";
|
||||
|
||||
if ($response) {
|
||||
echo json_encode(json_decode($response), JSON_PRETTY_PRINT);
|
||||
} else {
|
||||
echo "Failed to retrieve data.\n";
|
||||
}
|
26
MiroTalk SFU/app/api/meetings/meetings.py
Normal file
26
MiroTalk SFU/app/api/meetings/meetings.py
Normal file
@ -0,0 +1,26 @@
|
||||
# pip3 install requests
|
||||
import requests
|
||||
import json
|
||||
|
||||
API_KEY_SECRET = "mirotalksfu_default_secret"
|
||||
MIROTALK_URL = "https://sfu.mirotalk.com/api/v1/meetings"
|
||||
#MIROTALK_URL = "http://localhost:3010/api/v1/meetings"
|
||||
|
||||
headers = {
|
||||
"authorization": API_KEY_SECRET,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
response = requests.get(
|
||||
MIROTALK_URL,
|
||||
headers=headers
|
||||
)
|
||||
|
||||
print("Status code:", response.status_code)
|
||||
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
pretty_printed_data = json.dumps(data, indent=4)
|
||||
print(data)
|
||||
else:
|
||||
print("Failed to retrieve data. Error:", response.text)
|
10
MiroTalk SFU/app/api/meetings/meetings.sh
Normal file
10
MiroTalk SFU/app/api/meetings/meetings.sh
Normal file
@ -0,0 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
API_KEY_SECRET="mirotalksfu_default_secret"
|
||||
MIROTALK_URL="https://sfu.mirotalk.com/api/v1/meetings"
|
||||
#MIROTALK_URL="http://localhost:3010/api/v1/meetings"
|
||||
|
||||
curl $MIROTALK_URL \
|
||||
--header "authorization: $API_KEY_SECRET" \
|
||||
--header "Content-Type: application/json" \
|
||||
--request GET
|
BIN
MiroTalk SFU/app/api/restAPI.png
Normal file
BIN
MiroTalk SFU/app/api/restAPI.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
204
MiroTalk SFU/app/api/swagger.yaml
Normal file
204
MiroTalk SFU/app/api/swagger.yaml
Normal file
@ -0,0 +1,204 @@
|
||||
swagger: '2.0'
|
||||
|
||||
info:
|
||||
title: MiroTalk SFU API
|
||||
description: API description for external applications that integrate with MiroTalk SFU.
|
||||
version: 1.0.1
|
||||
|
||||
basePath: /api/v1
|
||||
|
||||
schemes:
|
||||
- https
|
||||
- http
|
||||
|
||||
paths:
|
||||
/meetings:
|
||||
get:
|
||||
tags:
|
||||
- 'meetings'
|
||||
summary: 'Get meetings'
|
||||
description: 'Get meetings'
|
||||
produces:
|
||||
- 'application/json'
|
||||
security:
|
||||
- secretApiKey: []
|
||||
responses:
|
||||
'200':
|
||||
description: 'Get Meetings done'
|
||||
schema:
|
||||
$ref: '#/definitions/MeetingsResponse'
|
||||
'403':
|
||||
description: 'Unauthorized!'
|
||||
/meeting:
|
||||
post:
|
||||
tags:
|
||||
- 'meeting'
|
||||
summary: 'Create meeting'
|
||||
description: 'Create meeting'
|
||||
consumes:
|
||||
- 'application/json'
|
||||
produces:
|
||||
- 'application/json'
|
||||
security:
|
||||
- secretApiKey: []
|
||||
responses:
|
||||
'200':
|
||||
description: 'Meeting created'
|
||||
schema:
|
||||
$ref: '#/definitions/MeetingResponse'
|
||||
'403':
|
||||
description: 'Unauthorized!'
|
||||
/join:
|
||||
post:
|
||||
tags:
|
||||
- 'join'
|
||||
summary: 'Create direct join'
|
||||
description: 'Create join'
|
||||
parameters:
|
||||
- in: body
|
||||
name: Join
|
||||
description: Custom Join URL.
|
||||
schema:
|
||||
$ref: '#/definitions/JoinRequest'
|
||||
consumes:
|
||||
- 'application/json'
|
||||
produces:
|
||||
- 'application/json'
|
||||
security:
|
||||
- secretApiKey: []
|
||||
responses:
|
||||
'200':
|
||||
description: 'Direct join created'
|
||||
schema:
|
||||
$ref: '#/definitions/JoinResponse'
|
||||
'403':
|
||||
description: 'Unauthorized!'
|
||||
/token:
|
||||
post:
|
||||
tags:
|
||||
- 'token'
|
||||
summary: 'Get token'
|
||||
description: 'Get token'
|
||||
parameters:
|
||||
- in: body
|
||||
name: token
|
||||
description: Custom Token.
|
||||
schema:
|
||||
$ref: '#/definitions/TokenRequest'
|
||||
consumes:
|
||||
- 'application/json'
|
||||
produces:
|
||||
- 'application/json'
|
||||
security:
|
||||
- secretApiKey: []
|
||||
responses:
|
||||
'200':
|
||||
description: 'Get token done'
|
||||
schema:
|
||||
$ref: '#/definitions/TokenResponse'
|
||||
'403':
|
||||
description: 'Unauthorized!'
|
||||
|
||||
securityDefinitions:
|
||||
secretApiKey:
|
||||
type: 'apiKey'
|
||||
name: 'authorization'
|
||||
in: 'header'
|
||||
description: 'Format like this: authorization: {API_KEY_SECRET}'
|
||||
|
||||
definitions:
|
||||
MeetingsResponse:
|
||||
type: object
|
||||
properties:
|
||||
meetings:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Meeting'
|
||||
MeetingResponse:
|
||||
type: 'object'
|
||||
properties:
|
||||
meeting:
|
||||
type: string
|
||||
JoinRequest:
|
||||
type: object
|
||||
properties:
|
||||
room:
|
||||
type: string
|
||||
default: 'test'
|
||||
roomPassword:
|
||||
type: ['boolean', 'string'] # Allow boolean or string type
|
||||
default: false
|
||||
name:
|
||||
type: string
|
||||
default: 'mirotalksfu'
|
||||
audio:
|
||||
type: boolean
|
||||
default: false
|
||||
video:
|
||||
type: boolean
|
||||
default: false
|
||||
screen:
|
||||
type: boolean
|
||||
default: false
|
||||
hide:
|
||||
type: boolean
|
||||
default: false
|
||||
notify:
|
||||
type: boolean
|
||||
default: false
|
||||
token:
|
||||
$ref: '#/definitions/TokenRequest'
|
||||
TokenRequest:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
default: 'username'
|
||||
password:
|
||||
type: string
|
||||
default: 'password'
|
||||
presenter:
|
||||
type: boolean
|
||||
default: true
|
||||
expire:
|
||||
type: string
|
||||
default: '1h'
|
||||
JoinResponse:
|
||||
type: 'object'
|
||||
properties:
|
||||
join:
|
||||
type: string
|
||||
TokenResponse:
|
||||
type: 'object'
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
Peer:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
presenter:
|
||||
type: boolean
|
||||
video:
|
||||
type: boolean
|
||||
audio:
|
||||
type: boolean
|
||||
screen:
|
||||
type: boolean
|
||||
hand:
|
||||
type: boolean
|
||||
os:
|
||||
type: string
|
||||
browser:
|
||||
type: string
|
||||
|
||||
Meeting:
|
||||
type: object
|
||||
properties:
|
||||
roomId:
|
||||
type: string
|
||||
peers:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Peer'
|
36
MiroTalk SFU/app/api/token/token.js
Normal file
36
MiroTalk SFU/app/api/token/token.js
Normal file
@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
|
||||
async function getToken() {
|
||||
try {
|
||||
// Use dynamic import with await
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
|
||||
const API_KEY_SECRET = 'mirotalksfu_default_secret';
|
||||
const MIROTALK_URL = 'https://sfu.mirotalk.com/api/v1/token';
|
||||
//const MIROTALK_URL = 'http://localhost:3010/api/v1/token';
|
||||
|
||||
const response = await fetch(MIROTALK_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
authorization: API_KEY_SECRET,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
presenter: true,
|
||||
expire: '1h',
|
||||
}),
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.error) {
|
||||
console.log('Error:', data.error);
|
||||
} else {
|
||||
console.log('token:', data.token);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
}
|
||||
|
||||
getToken();
|
37
MiroTalk SFU/app/api/token/token.php
Normal file
37
MiroTalk SFU/app/api/token/token.php
Normal file
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
$API_KEY_SECRET = "mirotalksfu_default_secret";
|
||||
$MIROTALK_URL = "https://sfu.mirotalk.com/api/v1/token";
|
||||
#$MIROTALK_URL = "http://localhost:3010/api/v1/token";
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $MIROTALK_URL);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
|
||||
$headers = [
|
||||
'authorization:' . $API_KEY_SECRET,
|
||||
'Content-Type: application/json'
|
||||
];
|
||||
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
$data = array(
|
||||
"username" => "username",
|
||||
"password" => "password",
|
||||
"presenter" => true,
|
||||
"expire" => "1h",
|
||||
);
|
||||
|
||||
$data_string = json_encode($data);
|
||||
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
echo "Status code: $httpcode \n";
|
||||
$data = json_decode($response);
|
||||
echo "token: ", $data->{'token'}, "\n";
|
29
MiroTalk SFU/app/api/token/token.py
Normal file
29
MiroTalk SFU/app/api/token/token.py
Normal file
@ -0,0 +1,29 @@
|
||||
# pip3 install requests
|
||||
import requests
|
||||
import json
|
||||
|
||||
API_KEY_SECRET = "mirotalksfu_default_secret"
|
||||
MIROTALK_URL = "https://sfu.mirotalk.com/api/v1/token"
|
||||
#MIROTALK_URL = "http://localhost:3010/api/v1/token"
|
||||
|
||||
headers = {
|
||||
"authorization": API_KEY_SECRET,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
data = {
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"presenter": "true",
|
||||
"expire": "1h"
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
MIROTALK_URL,
|
||||
headers=headers,
|
||||
json=data
|
||||
)
|
||||
|
||||
print("Status code:", response.status_code)
|
||||
data = json.loads(response.text)
|
||||
print("token:", data["token"])
|
11
MiroTalk SFU/app/api/token/token.sh
Normal file
11
MiroTalk SFU/app/api/token/token.sh
Normal file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
API_KEY_SECRET="mirotalksfu_default_secret"
|
||||
MIROTALK_URL="https://sfu.mirotalk.com/api/v1/token"
|
||||
#MIROTALK_URL="http://localhost:3010/api/v1/token"
|
||||
|
||||
curl $MIROTALK_URL \
|
||||
--header "authorization: $API_KEY_SECRET" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{"username":"username","password":"password","presenter":"true", "expire":"1h"}' \
|
||||
--request POST
|
42
MiroTalk SFU/app/src/Host.js
Normal file
42
MiroTalk SFU/app/src/Host.js
Normal file
@ -0,0 +1,42 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = class Host {
|
||||
constructor() {
|
||||
this.authorizedIPs = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authorized IPs
|
||||
* @returns object
|
||||
*/
|
||||
getAuthorizedIPs() {
|
||||
return Object.fromEntries(this.authorizedIPs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authorized IP
|
||||
* @param {string} ip
|
||||
* @param {boolean} authorized
|
||||
*/
|
||||
setAuthorizedIP(ip, authorized) {
|
||||
this.authorizedIPs.set(ip, authorized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if IP is authorized
|
||||
* @param {string} ip
|
||||
* @returns boolean
|
||||
*/
|
||||
isAuthorizedIP(ip) {
|
||||
return this.authorizedIPs.has(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete ip from authorized IPs
|
||||
* @param {string} ip
|
||||
* @returns boolean
|
||||
*/
|
||||
deleteIP(ip) {
|
||||
return this.authorizedIPs.delete(ip);
|
||||
}
|
||||
};
|
90
MiroTalk SFU/app/src/Logger.js
Normal file
90
MiroTalk SFU/app/src/Logger.js
Normal file
@ -0,0 +1,90 @@
|
||||
'use strict';
|
||||
|
||||
const util = require('util');
|
||||
|
||||
const colors = require('colors');
|
||||
|
||||
const config = require('./config');
|
||||
|
||||
config.console.colors ? colors.enable() : colors.disable();
|
||||
|
||||
const options = {
|
||||
depth: null,
|
||||
colors: true,
|
||||
};
|
||||
module.exports = class Logger {
|
||||
constructor(appName = 'miroTalkSfu') {
|
||||
this.appName = colors.yellow(appName);
|
||||
this.debugOn = config.console.debug;
|
||||
this.timeStart = Date.now();
|
||||
this.timeEnd = null;
|
||||
this.timeElapsedMs = null;
|
||||
this.tzOptions = {
|
||||
timeZone: process.env.TZ || config.console.timeZone || 'UTC',
|
||||
hour12: false,
|
||||
};
|
||||
}
|
||||
|
||||
debug(msg, op = '') {
|
||||
if (this.debugOn) {
|
||||
this.timeEnd = Date.now();
|
||||
this.timeElapsedMs = this.getFormatTime(Math.floor(this.timeEnd - this.timeStart));
|
||||
console.debug(
|
||||
'[' + this.getDateTime() + '] [' + this.appName + '] ' + msg,
|
||||
util.inspect(op, options),
|
||||
this.timeElapsedMs,
|
||||
);
|
||||
this.timeStart = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
log(msg, op = '') {
|
||||
console.log('[' + this.getDateTime() + '] [' + this.appName + '] ' + msg, util.inspect(op, options));
|
||||
}
|
||||
|
||||
info(msg, op = '') {
|
||||
console.info(
|
||||
'[' + this.getDateTime() + '] [' + this.appName + '] ' + colors.green(msg),
|
||||
util.inspect(op, options),
|
||||
);
|
||||
}
|
||||
|
||||
warn(msg, op = '') {
|
||||
console.warn(
|
||||
'[' + this.getDateTime() + '] [' + this.appName + '] ' + colors.yellow(msg),
|
||||
util.inspect(op, options),
|
||||
);
|
||||
}
|
||||
|
||||
error(msg, op = '') {
|
||||
console.error(
|
||||
'[' + this.getDateTime() + '] [' + this.appName + '] ' + colors.red(msg),
|
||||
util.inspect(op, options),
|
||||
);
|
||||
}
|
||||
|
||||
getDateTime() {
|
||||
const currentTime = new Date().toLocaleString('en-US', this.tzOptions);
|
||||
const milliseconds = String(new Date().getMilliseconds()).padStart(3, '0');
|
||||
return colors.cyan(`${currentTime}:${milliseconds}`);
|
||||
}
|
||||
|
||||
getFormatTime(ms) {
|
||||
let time = Math.floor(ms);
|
||||
let type = 'ms';
|
||||
|
||||
if (ms >= 1000) {
|
||||
time = Math.floor((ms / 1000) % 60);
|
||||
type = 's';
|
||||
}
|
||||
if (ms >= 60000) {
|
||||
time = Math.floor((ms / 1000 / 60) % 60);
|
||||
type = 'm';
|
||||
}
|
||||
if (ms >= (3, 6e6)) {
|
||||
time = Math.floor((ms / 1000 / 60 / 60) % 24);
|
||||
type = 'h';
|
||||
}
|
||||
return colors.magenta('+' + time + type);
|
||||
}
|
||||
};
|
316
MiroTalk SFU/app/src/Peer.js
Normal file
316
MiroTalk SFU/app/src/Peer.js
Normal file
@ -0,0 +1,316 @@
|
||||
'use strict';
|
||||
|
||||
const Logger = require('./Logger');
|
||||
const log = new Logger('Peer');
|
||||
|
||||
module.exports = class Peer {
|
||||
constructor(socket_id, data) {
|
||||
const { peer_info } = data;
|
||||
|
||||
const { peer_name, peer_presenter, peer_audio, peer_video, peer_video_privacy, peer_recording, peer_hand } =
|
||||
peer_info;
|
||||
|
||||
this.id = socket_id;
|
||||
this.peer_info = peer_info;
|
||||
this.peer_name = peer_name;
|
||||
this.peer_presenter = peer_presenter;
|
||||
this.peer_audio = peer_audio;
|
||||
this.peer_video = peer_video;
|
||||
this.peer_video_privacy = peer_video_privacy;
|
||||
this.peer_recording = peer_recording;
|
||||
this.peer_hand = peer_hand;
|
||||
|
||||
this.transports = new Map();
|
||||
this.consumers = new Map();
|
||||
this.producers = new Map();
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// UPDATE PEER INFO
|
||||
// ####################################################
|
||||
|
||||
updatePeerInfo(data) {
|
||||
log.debug('Update peer info', data);
|
||||
switch (data.type) {
|
||||
case 'audio':
|
||||
case 'audioType':
|
||||
this.peer_info.peer_audio = data.status;
|
||||
this.peer_audio = data.status;
|
||||
break;
|
||||
case 'video':
|
||||
case 'videoType':
|
||||
this.peer_info.peer_video = data.status;
|
||||
this.peer_video = data.status;
|
||||
if (data.status == false) {
|
||||
this.peer_info.peer_video_privacy = data.status;
|
||||
this.peer_video_privacy = data.status;
|
||||
}
|
||||
break;
|
||||
case 'screen':
|
||||
case 'screenType':
|
||||
this.peer_info.peer_screen = data.status;
|
||||
break;
|
||||
case 'hand':
|
||||
this.peer_info.peer_hand = data.status;
|
||||
this.peer_hand = data.status;
|
||||
break;
|
||||
case 'privacy':
|
||||
this.peer_info.peer_video_privacy = data.status;
|
||||
this.peer_video_privacy = data.status;
|
||||
break;
|
||||
case 'presenter':
|
||||
this.peer_info.peer_presenter = data.status;
|
||||
this.peer_presenter = data.status;
|
||||
break;
|
||||
case 'recording':
|
||||
this.peer_info.peer_recording = data.status;
|
||||
this.peer_recording = data.status;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// TRANSPORT
|
||||
// ####################################################
|
||||
|
||||
getTransports() {
|
||||
return JSON.parse(JSON.stringify([...this.transports]));
|
||||
}
|
||||
|
||||
getTransport(transport_id) {
|
||||
return this.transports.get(transport_id);
|
||||
}
|
||||
|
||||
delTransport(transport_id) {
|
||||
this.transports.delete(transport_id);
|
||||
}
|
||||
|
||||
addTransport(transport) {
|
||||
this.transports.set(transport.id, transport);
|
||||
}
|
||||
|
||||
async connectTransport(transport_id, dtlsParameters) {
|
||||
if (!this.transports.has(transport_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.transports.get(transport_id).connect({
|
||||
dtlsParameters: dtlsParameters,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.transports.forEach((transport, transport_id) => {
|
||||
log.debug('Close and delete peer transports', {
|
||||
transport_id: transport_id,
|
||||
//transportInternal: transport.internal,
|
||||
});
|
||||
transport.close();
|
||||
this.delTransport(transport_id);
|
||||
});
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// PRODUCER
|
||||
// ####################################################
|
||||
|
||||
getProducers() {
|
||||
return JSON.parse(JSON.stringify([...this.producers]));
|
||||
}
|
||||
|
||||
getProducer(producer_id) {
|
||||
return this.producers.get(producer_id);
|
||||
}
|
||||
|
||||
delProducer(producer_id) {
|
||||
this.producers.delete(producer_id);
|
||||
}
|
||||
|
||||
async createProducer(producerTransportId, producer_rtpParameters, producer_kind, producer_type) {
|
||||
try {
|
||||
if (!producerTransportId) {
|
||||
return 'Invalid producer transport ID';
|
||||
}
|
||||
|
||||
const producerTransport = this.transports.get(producerTransportId);
|
||||
|
||||
if (!producerTransport) {
|
||||
return `Producer transport with ID ${producerTransportId} not found`;
|
||||
}
|
||||
|
||||
const producer = await producerTransport.produce({
|
||||
kind: producer_kind,
|
||||
rtpParameters: producer_rtpParameters,
|
||||
});
|
||||
|
||||
if (!producer) {
|
||||
return `Producer type: ${producer_type} kind: ${producer_kind} not found`;
|
||||
}
|
||||
|
||||
const { id, appData, type, kind, rtpParameters } = producer;
|
||||
|
||||
appData.mediaType = producer_type;
|
||||
|
||||
this.producers.set(id, producer);
|
||||
|
||||
if (['simulcast', 'svc'].includes(type)) {
|
||||
const { scalabilityMode } = rtpParameters.encodings[0];
|
||||
const spatialLayer = parseInt(scalabilityMode.substring(1, 2)); // 1/2/3
|
||||
const temporalLayer = parseInt(scalabilityMode.substring(3, 4)); // 1/2/3
|
||||
log.debug(`Producer [${type}-${kind}] ----->`, {
|
||||
scalabilityMode,
|
||||
spatialLayer,
|
||||
temporalLayer,
|
||||
});
|
||||
} else {
|
||||
log.debug('Producer ----->', { type: type, kind: kind });
|
||||
}
|
||||
|
||||
producer.on('transportclose', () => {
|
||||
log.debug('Producer "transportclose" event');
|
||||
this.closeProducer(id);
|
||||
});
|
||||
|
||||
return producer;
|
||||
} catch (error) {
|
||||
log.error('Error creating producer', error.message);
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
closeProducer(producer_id) {
|
||||
if (!this.producers.has(producer_id)) return;
|
||||
|
||||
const producer = this.getProducer(producer_id);
|
||||
const { id, kind, type, appData } = producer;
|
||||
|
||||
try {
|
||||
producer.close();
|
||||
} catch (error) {
|
||||
log.warn('Close Producer', error.message);
|
||||
}
|
||||
|
||||
this.delProducer(producer_id);
|
||||
|
||||
log.debug('Producer closed and deleted', {
|
||||
peer_name: this.peer_name,
|
||||
kind: kind,
|
||||
type: type,
|
||||
appData: appData,
|
||||
producer_id: id,
|
||||
});
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// CONSUMER
|
||||
// ####################################################
|
||||
|
||||
getConsumers() {
|
||||
return JSON.parse(JSON.stringify([...this.consumers]));
|
||||
}
|
||||
|
||||
getConsumer(consumer_id) {
|
||||
return this.consumers.get(consumer_id);
|
||||
}
|
||||
|
||||
delConsumer(consumer_id) {
|
||||
this.consumers.delete(consumer_id);
|
||||
}
|
||||
|
||||
async createConsumer(consumer_transport_id, producer_id, rtpCapabilities) {
|
||||
try {
|
||||
if (!consumer_transport_id) {
|
||||
return 'Invalid consumer transport ID';
|
||||
}
|
||||
|
||||
const consumerTransport = this.transports.get(consumer_transport_id);
|
||||
|
||||
if (!consumerTransport) {
|
||||
return `Consumer transport with id ${consumer_transport_id} not found`;
|
||||
}
|
||||
|
||||
const consumer = await consumerTransport.consume({
|
||||
producerId: producer_id,
|
||||
rtpCapabilities,
|
||||
enableRtx: true, // Enable NACK for OPUS.
|
||||
paused: false,
|
||||
});
|
||||
|
||||
if (!consumer) {
|
||||
return `Consumer for producer ID ${producer_id} not found`;
|
||||
}
|
||||
|
||||
const { id, type, kind, rtpParameters, producerPaused } = consumer;
|
||||
|
||||
if (['simulcast', 'svc'].includes(type)) {
|
||||
// simulcast - L1T3/L2T3/L3T3 | svc - L3T3
|
||||
const { scalabilityMode } = rtpParameters.encodings[0];
|
||||
const spatialLayer = parseInt(scalabilityMode.substring(1, 2)); // 1/2/3
|
||||
const temporalLayer = parseInt(scalabilityMode.substring(3, 4)); // 1/2/3
|
||||
try {
|
||||
await consumer.setPreferredLayers({
|
||||
spatialLayer: spatialLayer,
|
||||
temporalLayer: temporalLayer,
|
||||
});
|
||||
log.debug(`Consumer [${type}-${kind}] ----->`, {
|
||||
scalabilityMode,
|
||||
spatialLayer,
|
||||
temporalLayer,
|
||||
});
|
||||
} catch (error) {
|
||||
return `Error to set Consumer preferred layers: ${error.message}`;
|
||||
}
|
||||
} else {
|
||||
log.debug('Consumer ----->', { type: type, kind: kind });
|
||||
}
|
||||
|
||||
this.consumers.set(id, consumer);
|
||||
|
||||
consumer.on('transportclose', () => {
|
||||
log.debug('Consumer "transportclose" event');
|
||||
this.removeConsumer(id);
|
||||
});
|
||||
|
||||
return {
|
||||
consumer: consumer,
|
||||
params: {
|
||||
producerId: producer_id,
|
||||
id: id,
|
||||
kind: kind,
|
||||
rtpParameters: rtpParameters,
|
||||
type: type,
|
||||
producerPaused: producerPaused,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
log.error('Error creating consumer', error.message);
|
||||
return error.message;
|
||||
}
|
||||
}
|
||||
|
||||
removeConsumer(consumer_id) {
|
||||
if (!this.consumers.has(consumer_id)) return;
|
||||
|
||||
const consumer = this.getConsumer(consumer_id);
|
||||
const { id, kind, type } = consumer;
|
||||
|
||||
try {
|
||||
consumer.close();
|
||||
} catch (error) {
|
||||
log.warn('Close Consumer', error.message);
|
||||
}
|
||||
|
||||
this.delConsumer(consumer_id);
|
||||
|
||||
log.debug('Consumer closed and deleted', {
|
||||
peer_name: this.peer_name,
|
||||
kind: kind,
|
||||
type: type,
|
||||
consumer_id: id,
|
||||
});
|
||||
}
|
||||
};
|
577
MiroTalk SFU/app/src/Room.js
Normal file
577
MiroTalk SFU/app/src/Room.js
Normal file
@ -0,0 +1,577 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
const Logger = require('./Logger');
|
||||
const log = new Logger('Room');
|
||||
|
||||
module.exports = class Room {
|
||||
constructor(room_id, worker, io) {
|
||||
this.id = room_id;
|
||||
this.worker = worker;
|
||||
this.webRtcServer = worker.appData.webRtcServer;
|
||||
this.webRtcServerActive = config.mediasoup.webRtcServerActive;
|
||||
this.io = io;
|
||||
this.audioLevelObserver = null;
|
||||
this.audioLevelObserverEnabled = true;
|
||||
this.audioLastUpdateTime = 0;
|
||||
// ##########################
|
||||
this._isBroadcasting = false;
|
||||
// ##########################
|
||||
this._isLocked = false;
|
||||
this._isLobbyEnabled = false;
|
||||
this._roomPassword = null;
|
||||
this._hostOnlyRecording = false;
|
||||
// ##########################
|
||||
this._recSyncServerRecording = config?.server?.recording?.enabled || false;
|
||||
// ##########################
|
||||
this._moderator = {
|
||||
audio_start_muted: false,
|
||||
video_start_hidden: false,
|
||||
audio_cant_unmute: false,
|
||||
video_cant_unhide: false,
|
||||
screen_cant_share: false,
|
||||
chat_cant_privately: false,
|
||||
chat_cant_chatgpt: false,
|
||||
};
|
||||
this.survey = config.survey;
|
||||
this.redirect = config.redirect;
|
||||
this.peers = new Map();
|
||||
this.bannedPeers = [];
|
||||
this.webRtcTransport = config.mediasoup.webRtcTransport;
|
||||
this.router = null;
|
||||
this.routerSettings = config.mediasoup.router;
|
||||
this.createTheRouter();
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ROOM INFO
|
||||
// ####################################################
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
id: this.id,
|
||||
broadcasting: this._isBroadcasting,
|
||||
recSyncServerRecording: this._recSyncServerRecording,
|
||||
config: {
|
||||
isLocked: this._isLocked,
|
||||
isLobbyEnabled: this._isLobbyEnabled,
|
||||
hostOnlyRecording: this._hostOnlyRecording,
|
||||
},
|
||||
moderator: this._moderator,
|
||||
survey: this.survey,
|
||||
redirect: this.redirect,
|
||||
peers: JSON.stringify([...this.peers]),
|
||||
};
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ROUTER
|
||||
// ####################################################
|
||||
|
||||
createTheRouter() {
|
||||
const { mediaCodecs } = this.routerSettings;
|
||||
this.worker
|
||||
.createRouter({
|
||||
mediaCodecs,
|
||||
})
|
||||
.then((router) => {
|
||||
this.router = router;
|
||||
if (this.audioLevelObserverEnabled) {
|
||||
this.startAudioLevelObservation();
|
||||
}
|
||||
this.router.observer.on('close', () => {
|
||||
log.info('---------------> Router is now closed as the last peer has left the room', {
|
||||
room: this.id,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closeRouter() {
|
||||
this.router.close();
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// PRODUCER AUDIO LEVEL OBSERVER
|
||||
// ####################################################
|
||||
|
||||
async startAudioLevelObservation() {
|
||||
log.debug('Start audioLevelObserver for signaling active speaker...');
|
||||
|
||||
this.audioLevelObserver = await this.router.createAudioLevelObserver({
|
||||
maxEntries: 1,
|
||||
threshold: -70,
|
||||
interval: 100,
|
||||
});
|
||||
|
||||
this.audioLevelObserver.on('volumes', (volumes) => {
|
||||
this.sendActiveSpeakerVolume(volumes);
|
||||
});
|
||||
this.audioLevelObserver.on('silence', () => {
|
||||
//log.debug('audioLevelObserver', { volume: 'silence' });
|
||||
});
|
||||
}
|
||||
|
||||
sendActiveSpeakerVolume(volumes) {
|
||||
try {
|
||||
if (!Array.isArray(volumes) || volumes.length === 0) {
|
||||
throw new Error('Invalid volumes array');
|
||||
}
|
||||
|
||||
if (Date.now() > this.audioLastUpdateTime + 100) {
|
||||
this.audioLastUpdateTime = Date.now();
|
||||
|
||||
const { producer, volume } = volumes[0];
|
||||
const audioVolume = Math.round(Math.pow(10, volume / 70) * 10); // Scale volume to 1-10
|
||||
|
||||
if (audioVolume > 1) {
|
||||
this.peers.forEach((peer) => {
|
||||
const { id, peer_audio, peer_name } = peer;
|
||||
peer.producers.forEach((peerProducer) => {
|
||||
if (peerProducer.id === producer.id && peerProducer.kind === 'audio' && peer_audio) {
|
||||
const data = {
|
||||
peer_id: id,
|
||||
peer_name: peer_name,
|
||||
audioVolume: audioVolume,
|
||||
};
|
||||
// Uncomment the following line for debugging
|
||||
// log.debug('Sending audio volume', data);
|
||||
this.sendToAll('audioVolume', data);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
log.error('Error sending active speaker volume', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
addProducerToAudioLevelObserver(producer) {
|
||||
if (this.audioLevelObserverEnabled) {
|
||||
this.audioLevelObserver.addProducer(producer);
|
||||
}
|
||||
}
|
||||
|
||||
getRtpCapabilities() {
|
||||
return this.router.rtpCapabilities;
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ROOM MODERATOR
|
||||
// ####################################################
|
||||
|
||||
updateRoomModeratorALL(data) {
|
||||
this._moderator = data;
|
||||
log.debug('Update room moderator all data', this._moderator);
|
||||
}
|
||||
|
||||
updateRoomModerator(data) {
|
||||
log.debug('Update room moderator', data);
|
||||
switch (data.type) {
|
||||
case 'audio_start_muted':
|
||||
this._moderator.audio_start_muted = data.status;
|
||||
break;
|
||||
case 'video_start_hidden':
|
||||
this._moderator.video_start_hidden = data.status;
|
||||
case 'audio_cant_unmute':
|
||||
this._moderator.audio_cant_unmute = data.status;
|
||||
break;
|
||||
case 'video_cant_unhide':
|
||||
this._moderator.video_cant_unhide = data.status;
|
||||
case 'screen_cant_share':
|
||||
this._moderator.screen_cant_share = data.status;
|
||||
break;
|
||||
case 'chat_cant_privately':
|
||||
this._moderator.chat_cant_privately = data.status;
|
||||
break;
|
||||
case 'chat_cant_chatgpt':
|
||||
this._moderator.chat_cant_chatgpt = data.status;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// PEERS
|
||||
// ####################################################
|
||||
|
||||
addPeer(peer) {
|
||||
this.peers.set(peer.id, peer);
|
||||
}
|
||||
|
||||
getPeer(socket_id) {
|
||||
//
|
||||
if (!this.peers.has(socket_id)) {
|
||||
log.error('---> Peer not found for socket ID', socket_id);
|
||||
return null;
|
||||
}
|
||||
|
||||
const peer = this.peers.get(socket_id);
|
||||
|
||||
return peer;
|
||||
}
|
||||
|
||||
getPeers() {
|
||||
return this.peers;
|
||||
}
|
||||
|
||||
getPeersCount() {
|
||||
return this.peers.size;
|
||||
}
|
||||
|
||||
getProducerListForPeer() {
|
||||
const producerList = [];
|
||||
this.peers.forEach((peer) => {
|
||||
const { peer_name, peer_info } = peer;
|
||||
peer.producers.forEach((producer) => {
|
||||
producerList.push({
|
||||
producer_id: producer.id,
|
||||
peer_name: peer_name,
|
||||
peer_info: peer_info,
|
||||
type: producer.appData.mediaType,
|
||||
});
|
||||
});
|
||||
});
|
||||
return producerList;
|
||||
}
|
||||
|
||||
async removePeer(socket_id) {
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, peer_name } = peer;
|
||||
|
||||
peer.close();
|
||||
|
||||
this.peers.delete(socket_id);
|
||||
|
||||
if (this.getPeers().size === 0) {
|
||||
this.closeRouter();
|
||||
}
|
||||
|
||||
const peerTransports = peer.getTransports();
|
||||
const peerProducers = peer.getProducers();
|
||||
const peerConsumers = peer.getConsumers();
|
||||
|
||||
log.debug('REMOVE PEER', {
|
||||
peer_id: id,
|
||||
peer_name: peer_name,
|
||||
peerTransports: peerTransports,
|
||||
peerProducers: peerProducers,
|
||||
peerConsumers: peerConsumers,
|
||||
});
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// WebRTC TRANSPORT
|
||||
// ####################################################
|
||||
|
||||
async createWebRtcTransport(socket_id) {
|
||||
const { maxIncomingBitrate, initialAvailableOutgoingBitrate, listenInfos } = this.webRtcTransport;
|
||||
|
||||
const webRtcTransportOptions = {
|
||||
...(this.webRtcServerActive ? { webRtcServer: this.webRtcServer } : { listenInfos: listenInfos }),
|
||||
enableUdp: true,
|
||||
enableTcp: true,
|
||||
preferUdp: true,
|
||||
iceConsentTimeout: 20,
|
||||
initialAvailableOutgoingBitrate,
|
||||
};
|
||||
|
||||
log.debug('webRtcTransportOptions ----->', webRtcTransportOptions);
|
||||
|
||||
const transport = await this.router.createWebRtcTransport(webRtcTransportOptions);
|
||||
|
||||
if (!transport) {
|
||||
return this.callback('[Room|createWebRtcTransport] Failed to create WebRTC transport');
|
||||
}
|
||||
|
||||
const { id, iceParameters, iceCandidates, dtlsParameters } = transport;
|
||||
|
||||
if (maxIncomingBitrate) {
|
||||
try {
|
||||
await transport.setMaxIncomingBitrate(maxIncomingBitrate);
|
||||
} catch (error) {
|
||||
log.debug('Transport setMaxIncomingBitrate error', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return this.callback(`[Room|createWebRtcTransport] Peer object not found for socket ID: ${socket_id}`);
|
||||
}
|
||||
|
||||
const { peer_name } = peer;
|
||||
|
||||
transport.on('icestatechange', (iceState) => {
|
||||
if (iceState === 'disconnected' || iceState === 'closed') {
|
||||
log.debug('Transport closed "icestatechange" event', {
|
||||
peer_name: peer_name,
|
||||
transport_id: id,
|
||||
iceState: iceState,
|
||||
});
|
||||
transport.close();
|
||||
}
|
||||
});
|
||||
|
||||
transport.on('sctpstatechange', (sctpState) => {
|
||||
log.debug('Transport "sctpstatechange" event', {
|
||||
peer_name: peer_name,
|
||||
transport_id: id,
|
||||
sctpState: sctpState,
|
||||
});
|
||||
});
|
||||
|
||||
transport.on('dtlsstatechange', (dtlsState) => {
|
||||
if (dtlsState === 'failed' || dtlsState === 'closed') {
|
||||
log.debug('Transport closed "dtlsstatechange" event', {
|
||||
peer_name: peer_name,
|
||||
transport_id: id,
|
||||
dtlsState: dtlsState,
|
||||
});
|
||||
transport.close();
|
||||
}
|
||||
});
|
||||
|
||||
transport.on('close', () => {
|
||||
log.debug('Transport closed', { peer_name: peer_name, transport_id: transport.id });
|
||||
});
|
||||
|
||||
peer.addTransport(transport);
|
||||
|
||||
log.debug('Transport created', { transportId: id });
|
||||
|
||||
return {
|
||||
id: id,
|
||||
iceParameters: iceParameters,
|
||||
iceCandidates: iceCandidates,
|
||||
dtlsParameters: dtlsParameters,
|
||||
};
|
||||
}
|
||||
|
||||
async connectPeerTransport(socket_id, transport_id, dtlsParameters) {
|
||||
try {
|
||||
if (!socket_id || !transport_id || !dtlsParameters) {
|
||||
return this.callback('[Room|connectPeerTransport] Invalid input parameters');
|
||||
}
|
||||
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return this.callback(`[Room|connectPeerTransport] Peer object not found for socket ID: ${socket_id}`);
|
||||
}
|
||||
|
||||
const connectTransport = await peer.connectTransport(transport_id, dtlsParameters);
|
||||
|
||||
if (!connectTransport) {
|
||||
return this.callback(`[Room|connectPeerTransport] error: Transport with ID ${transport_id} not found`);
|
||||
}
|
||||
|
||||
return '[Room|connectPeerTransport] done';
|
||||
} catch (error) {
|
||||
log.error('Error connecting peer transport', error.message);
|
||||
return this.callback(`[Room|connectPeerTransport] error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// PRODUCE
|
||||
// ####################################################
|
||||
|
||||
async produce(socket_id, producerTransportId, rtpParameters, kind, type) {
|
||||
//
|
||||
if (!socket_id || !producerTransportId || !rtpParameters || !kind || !type) {
|
||||
return this.callback('[Room|produce] Invalid input parameters');
|
||||
}
|
||||
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return this.callback(`[Room|produce] Peer object not found for socket ID: ${socket_id}`);
|
||||
}
|
||||
|
||||
const peerProducer = await peer.createProducer(producerTransportId, rtpParameters, kind, type);
|
||||
|
||||
if (!peerProducer || !peerProducer.id) {
|
||||
return this.callback(`[Room|produce] Peer producer error: '${peerProducer}'`);
|
||||
}
|
||||
|
||||
const { id } = peerProducer;
|
||||
|
||||
const { peer_name, peer_info } = peer;
|
||||
|
||||
this.broadCast(socket_id, 'newProducers', [
|
||||
{
|
||||
producer_id: id,
|
||||
producer_socket_id: socket_id,
|
||||
peer_name: peer_name,
|
||||
peer_info: peer_info,
|
||||
type: type,
|
||||
},
|
||||
]);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
closeProducer(socket_id, producer_id) {
|
||||
if (!socket_id || !producer_id) return;
|
||||
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return;
|
||||
}
|
||||
|
||||
peer.closeProducer(producer_id);
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// CONSUME
|
||||
// ####################################################
|
||||
|
||||
async consume(socket_id, consumer_transport_id, producer_id, rtpCapabilities) {
|
||||
try {
|
||||
if (!socket_id || !consumer_transport_id || !producer_id || !rtpCapabilities) {
|
||||
return this.callback('[Room|consume] Invalid input parameters');
|
||||
}
|
||||
|
||||
if (!this.router.canConsume({ producerId: producer_id, rtpCapabilities })) {
|
||||
log.warn('Cannot consume', {
|
||||
socket_id,
|
||||
consumer_transport_id,
|
||||
producer_id,
|
||||
});
|
||||
return this.callback(`[Room|consume] Room router cannot consume producer_id: '${producer_id}'`);
|
||||
}
|
||||
|
||||
const peer = this.getPeer(socket_id);
|
||||
|
||||
if (!peer || typeof peer !== 'object') {
|
||||
return this.callback(`[Room|consume] Peer object not found for socket ID: ${socket_id}`);
|
||||
}
|
||||
|
||||
const peerConsumer = await peer.createConsumer(consumer_transport_id, producer_id, rtpCapabilities);
|
||||
|
||||
if (!peerConsumer || !peerConsumer.consumer || !peerConsumer.params) {
|
||||
log.debug('peerConsumer or params are not defined');
|
||||
return this.callback(`[Room|consume] peerConsumer error: '${peerConsumer}'`);
|
||||
}
|
||||
|
||||
const { consumer, params } = peerConsumer;
|
||||
|
||||
const { id, kind } = consumer;
|
||||
|
||||
consumer.on('producerclose', () => {
|
||||
log.debug('Consumer closed due to "producerclose" event');
|
||||
peer.removeConsumer(id);
|
||||
|
||||
// Notify the client that consumer is closed
|
||||
this.send(socket_id, 'consumerClosed', {
|
||||
consumer_id: id,
|
||||
consumer_kind: kind,
|
||||
});
|
||||
});
|
||||
|
||||
return params;
|
||||
} catch (error) {
|
||||
log.error('Error occurred during consumption', error.message);
|
||||
return this.callback(`[Room|consume] ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// HANDLE BANNED PEERS
|
||||
// ####################################################
|
||||
|
||||
addBannedPeer(uuid) {
|
||||
if (!this.bannedPeers.includes(uuid)) {
|
||||
this.bannedPeers.push(uuid);
|
||||
log.debug('Added to the banned list', {
|
||||
uuid: uuid,
|
||||
banned: this.bannedPeers,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isBanned(uuid) {
|
||||
return this.bannedPeers.includes(uuid);
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ROOM STATUS
|
||||
// ####################################################
|
||||
|
||||
// GET
|
||||
isBroadcasting() {
|
||||
return this._isBroadcasting;
|
||||
}
|
||||
getPassword() {
|
||||
return this._roomPassword;
|
||||
}
|
||||
|
||||
// BOOL
|
||||
isLocked() {
|
||||
return this._isLocked;
|
||||
}
|
||||
isLobbyEnabled() {
|
||||
return this._isLobbyEnabled;
|
||||
}
|
||||
isHostOnlyRecording() {
|
||||
return this._hostOnlyRecording;
|
||||
}
|
||||
|
||||
// SET
|
||||
setIsBroadcasting(status) {
|
||||
this._isBroadcasting = status;
|
||||
}
|
||||
setLocked(status, password) {
|
||||
this._isLocked = status;
|
||||
this._roomPassword = password;
|
||||
}
|
||||
setLobbyEnabled(status) {
|
||||
this._isLobbyEnabled = status;
|
||||
}
|
||||
setHostOnlyRecording(status) {
|
||||
this._hostOnlyRecording = status;
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// ERRORS
|
||||
// ####################################################
|
||||
|
||||
callback(message) {
|
||||
return { error: message };
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// SENDER
|
||||
// ####################################################
|
||||
|
||||
broadCast(socket_id, action, data) {
|
||||
for (let otherID of Array.from(this.peers.keys()).filter((id) => id !== socket_id)) {
|
||||
this.send(otherID, action, data);
|
||||
}
|
||||
}
|
||||
|
||||
sendTo(socket_id, action, data) {
|
||||
for (let peer_id of Array.from(this.peers.keys()).filter((id) => id === socket_id)) {
|
||||
this.send(peer_id, action, data);
|
||||
}
|
||||
}
|
||||
|
||||
sendToAll(action, data) {
|
||||
for (let peer_id of Array.from(this.peers.keys())) {
|
||||
this.send(peer_id, action, data);
|
||||
}
|
||||
}
|
||||
|
||||
send(socket_id, action, data) {
|
||||
this.io.to(socket_id).emit(action, data);
|
||||
}
|
||||
};
|
2058
MiroTalk SFU/app/src/Server.js
Normal file
2058
MiroTalk SFU/app/src/Server.js
Normal file
File diff suppressed because it is too large
Load Diff
121
MiroTalk SFU/app/src/ServerApi.js
Normal file
121
MiroTalk SFU/app/src/ServerApi.js
Normal file
@ -0,0 +1,121 @@
|
||||
'use strict';
|
||||
|
||||
const jwt = require('jsonwebtoken');
|
||||
const CryptoJS = require('crypto-js');
|
||||
|
||||
const config = require('./config');
|
||||
const { v4: uuidV4 } = require('uuid');
|
||||
|
||||
const JWT_KEY = (config.jwt && config.jwt.key) || 'mirotalksfu_jwt_secret';
|
||||
const JWT_EXP = (config.jwt && config.jwt.exp) || '1h';
|
||||
|
||||
module.exports = class ServerApi {
|
||||
constructor(host = null, authorization = null) {
|
||||
this._host = host;
|
||||
this._authorization = authorization;
|
||||
this._api_key_secret = config.api.keySecret;
|
||||
}
|
||||
|
||||
isAuthorized() {
|
||||
if (this._authorization != this._api_key_secret) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
getMeetings(roomList) {
|
||||
const meetings = Array.from(roomList.entries()).map(([id, room]) => {
|
||||
const peers = Array.from(room.peers.values()).map(
|
||||
({
|
||||
peer_info: {
|
||||
peer_name,
|
||||
peer_presenter,
|
||||
peer_video,
|
||||
peer_audio,
|
||||
peer_screen,
|
||||
peer_hand,
|
||||
os_name,
|
||||
os_version,
|
||||
browser_name,
|
||||
browser_version,
|
||||
},
|
||||
}) => ({
|
||||
name: peer_name,
|
||||
presenter: peer_presenter,
|
||||
video: peer_video,
|
||||
audio: peer_audio,
|
||||
screen: peer_screen,
|
||||
hand: peer_hand,
|
||||
os: os_name ? `${os_name} ${os_version}` : '',
|
||||
browser: browser_name ? `${browser_name} ${browser_version}` : '',
|
||||
}),
|
||||
);
|
||||
return {
|
||||
roomId: id,
|
||||
peers: peers,
|
||||
};
|
||||
});
|
||||
return meetings;
|
||||
}
|
||||
|
||||
getMeetingURL() {
|
||||
return 'https://' + this._host + '/join/' + uuidV4();
|
||||
}
|
||||
|
||||
getJoinURL(data) {
|
||||
// Get data
|
||||
const { room, roomPassword, name, audio, video, screen, hide, notify, token } = data;
|
||||
|
||||
const roomValue = room || uuidV4();
|
||||
const nameValue = name || 'User-' + this.getRandomNumber();
|
||||
const roomPasswordValue = roomPassword || false;
|
||||
const audioValue = audio || false;
|
||||
const videoValue = video || false;
|
||||
const screenValue = screen || false;
|
||||
const hideValue = hide || false;
|
||||
const notifyValue = notify || false;
|
||||
const jwtToken = token ? '&token=' + this.getToken(token) : '';
|
||||
|
||||
const joinURL =
|
||||
'https://' +
|
||||
this._host +
|
||||
'/join?' +
|
||||
`room=${roomValue}` +
|
||||
`&roomPassword=${roomPasswordValue}` +
|
||||
`&name=${encodeURIComponent(nameValue)}` +
|
||||
`&audio=${audioValue}` +
|
||||
`&video=${videoValue}` +
|
||||
`&screen=${screenValue}` +
|
||||
`&hide=${hideValue}` +
|
||||
`¬ify=${notifyValue}` +
|
||||
jwtToken;
|
||||
|
||||
return joinURL;
|
||||
}
|
||||
|
||||
getToken(token) {
|
||||
if (!token) return '';
|
||||
|
||||
const { username = 'username', password = 'password', presenter = false, expire } = token;
|
||||
|
||||
const expireValue = expire || JWT_EXP;
|
||||
|
||||
// Constructing payload
|
||||
const payload = {
|
||||
username: String(username),
|
||||
password: String(password),
|
||||
presenter: String(presenter),
|
||||
};
|
||||
|
||||
// Encrypt payload using AES encryption
|
||||
const payloadString = JSON.stringify(payload);
|
||||
const encryptedPayload = CryptoJS.AES.encrypt(payloadString, JWT_KEY).toString();
|
||||
|
||||
// Constructing JWT token
|
||||
const jwtToken = jwt.sign({ data: encryptedPayload }, JWT_KEY, { expiresIn: expireValue });
|
||||
|
||||
return jwtToken;
|
||||
}
|
||||
|
||||
getRandomNumber() {
|
||||
return Math.floor(Math.random() * 999999);
|
||||
}
|
||||
};
|
66
MiroTalk SFU/app/src/XSS.js
Normal file
66
MiroTalk SFU/app/src/XSS.js
Normal file
@ -0,0 +1,66 @@
|
||||
'use strict';
|
||||
|
||||
const xss = require('xss');
|
||||
const Logger = require('./Logger');
|
||||
const log = new Logger('Xss');
|
||||
|
||||
const checkXSS = (dataObject) => {
|
||||
try {
|
||||
if (Array.isArray(dataObject)) {
|
||||
if (Object.keys(dataObject).length > 0 && typeof dataObject[0] === 'object') {
|
||||
dataObject.forEach((obj) => {
|
||||
for (const key in obj) {
|
||||
if (obj.hasOwnProperty(key)) {
|
||||
let objectJson = objectToJSONString(obj[key]);
|
||||
if (objectJson) {
|
||||
let jsonString = xss(objectJson);
|
||||
let jsonObject = JSONStringToObject(jsonString);
|
||||
if (jsonObject) {
|
||||
obj[key] = jsonObject;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
log.debug('XSS Array of Object sanitization done');
|
||||
return dataObject;
|
||||
}
|
||||
} else if (typeof dataObject === 'object') {
|
||||
let objectJson = objectToJSONString(dataObject);
|
||||
if (objectJson) {
|
||||
let jsonString = xss(objectJson);
|
||||
let jsonObject = JSONStringToObject(jsonString);
|
||||
if (jsonObject) {
|
||||
log.debug('XSS Object sanitization done');
|
||||
return jsonObject;
|
||||
}
|
||||
}
|
||||
} else if (typeof dataObject === 'string' || dataObject instanceof String) {
|
||||
log.debug('XSS String sanitization done');
|
||||
return xss(dataObject);
|
||||
}
|
||||
log.warn('XSS not sanitized', dataObject);
|
||||
return dataObject;
|
||||
} catch (error) {
|
||||
log.error('XSS error', { data: dataObject, error: error });
|
||||
return dataObject;
|
||||
}
|
||||
};
|
||||
|
||||
function objectToJSONString(dataObject) {
|
||||
try {
|
||||
return JSON.stringify(dataObject);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function JSONStringToObject(jsonString) {
|
||||
try {
|
||||
return JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = checkXSS;
|
433
MiroTalk SFU/app/src/config.template.js
Normal file
433
MiroTalk SFU/app/src/config.template.js
Normal file
@ -0,0 +1,433 @@
|
||||
'use strict';
|
||||
|
||||
const os = require('os');
|
||||
|
||||
// https://api.ipify.org
|
||||
|
||||
function getIPv4() {
|
||||
const ifaces = os.networkInterfaces();
|
||||
for (const interfaceName in ifaces) {
|
||||
const iface = ifaces[interfaceName];
|
||||
for (const { address, family, internal } of iface) {
|
||||
if (family === 'IPv4' && !internal) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return '0.0.0.0'; // Default to 0.0.0.0 if no external IPv4 address found
|
||||
}
|
||||
|
||||
const IPv4 = getIPv4();
|
||||
|
||||
const numWorkers = require('os').cpus().length;
|
||||
|
||||
module.exports = {
|
||||
console: {
|
||||
/*
|
||||
timeZone: Time Zone corresponding to timezone identifiers from the IANA Time Zone Database es 'Europe/Rome' default UTC
|
||||
*/
|
||||
timeZone: 'UTC',
|
||||
debug: true,
|
||||
colors: true,
|
||||
},
|
||||
server: {
|
||||
listen: {
|
||||
// app listen on
|
||||
ip: '0.0.0.0',
|
||||
port: process.env.PORT || 3010,
|
||||
},
|
||||
ssl: {
|
||||
// ssl/README.md
|
||||
cert: '../ssl/cert.pem',
|
||||
key: '../ssl/key.pem',
|
||||
},
|
||||
cors: {
|
||||
/*
|
||||
origin: Allow specified origin es ['https://example.com', 'https://subdomain.example.com', 'http://localhost:3010'] or all origins if not specified
|
||||
methods: Allow only GET and POST methods
|
||||
*/
|
||||
origin: '*',
|
||||
methods: ['GET', 'POST'],
|
||||
},
|
||||
recording: {
|
||||
/*
|
||||
The recording will be saved to the directory designated within your Server app/<dir>
|
||||
Note: if you use Docker: Create the "app/rec" directory, configure it as a volume in docker-compose.yml,
|
||||
ensure proper permissions, and start the Docker container.
|
||||
*/
|
||||
dir: 'rec',
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
middleware: {
|
||||
/*
|
||||
Middleware:
|
||||
- IP Whitelist: Access to the instance is restricted to only the specified IP addresses in the allowed list. This feature is disabled by default.
|
||||
- ...
|
||||
*/
|
||||
IpWhitelist: {
|
||||
enabled: false,
|
||||
allowed: ['127.0.0.1', '::1'],
|
||||
},
|
||||
},
|
||||
api: {
|
||||
// Default secret key for app/api
|
||||
keySecret: 'mirotalksfu_default_secret',
|
||||
// Define which endpoints are allowed
|
||||
allowed: {
|
||||
meetings: false,
|
||||
meeting: true,
|
||||
join: true,
|
||||
token: false,
|
||||
slack: true,
|
||||
//...
|
||||
},
|
||||
},
|
||||
jwt: {
|
||||
/*
|
||||
JWT https://jwt.io/
|
||||
Securely manages credentials for host configurations and user authentication, enhancing security and streamlining processes.
|
||||
*/
|
||||
key: 'mirotalksfu_jwt_secret',
|
||||
exp: '1h',
|
||||
},
|
||||
host: {
|
||||
/*
|
||||
Host Protection (default: false)
|
||||
To enhance host security, enable host protection - user auth and provide valid
|
||||
usernames and passwords in the users array or active users_from_db using users_api_endpoint for check.
|
||||
*/
|
||||
protected: false,
|
||||
user_auth: false,
|
||||
users_from_db: false, // if true ensure that api.token is also set to true.
|
||||
//users_api_endpoint: 'http://localhost:9000/api/v1/user/isAuth',
|
||||
users_api_endpoint: 'https://webrtc.mirotalk.com/api/v1/user/isAuth',
|
||||
users_api_secret_key: 'mirotalkweb_default_secret',
|
||||
users: [
|
||||
{
|
||||
username: 'username',
|
||||
password: 'password',
|
||||
},
|
||||
{
|
||||
username: 'username2',
|
||||
password: 'password2',
|
||||
},
|
||||
//...
|
||||
],
|
||||
},
|
||||
presenters: {
|
||||
list: [
|
||||
/*
|
||||
By default, the presenter is identified as the first participant to join the room, distinguished by their username and UUID.
|
||||
Additional layers can be added to specify valid presenters and co-presenters by setting designated usernames.
|
||||
*/
|
||||
'Miroslav Pejic',
|
||||
'miroslav.pejic.85@gmail.com',
|
||||
],
|
||||
join_first: true, // Set to true for traditional behavior, false to prioritize presenters
|
||||
},
|
||||
chatGPT: {
|
||||
/*
|
||||
ChatGPT
|
||||
1. Goto https://platform.openai.com/
|
||||
2. Create your account
|
||||
3. Generate your APIKey https://platform.openai.com/account/api-keys
|
||||
*/
|
||||
enabled: false,
|
||||
basePath: 'https://api.openai.com/v1/',
|
||||
apiKey: '',
|
||||
model: 'gpt-3.5-turbo',
|
||||
max_tokens: 1000,
|
||||
temperature: 0,
|
||||
},
|
||||
email: {
|
||||
/*
|
||||
Configure email settings for notifications or alerts
|
||||
Refer to the documentation for Gmail configuration: https://support.google.com/mail/answer/185833?hl=en
|
||||
*/
|
||||
alert: false,
|
||||
host: 'smtp.gmail.com',
|
||||
port: 587,
|
||||
username: 'your_username',
|
||||
password: 'your_password',
|
||||
sendTo: 'sfu.mirotalk@gmail.com',
|
||||
},
|
||||
ngrok: {
|
||||
/*
|
||||
Ngrok
|
||||
1. Goto https://ngrok.com
|
||||
2. Get started for free
|
||||
3. Copy YourNgrokAuthToken: https://dashboard.ngrok.com/get-started/your-authtoken
|
||||
*/
|
||||
authToken: '',
|
||||
},
|
||||
sentry: {
|
||||
/*
|
||||
Sentry
|
||||
1. Goto https://sentry.io/
|
||||
2. Create account
|
||||
3. On dashboard goto Settings/Projects/YourProjectName/Client Keys (DSN)
|
||||
*/
|
||||
enabled: false,
|
||||
DSN: '',
|
||||
tracesSampleRate: 0.5,
|
||||
},
|
||||
slack: {
|
||||
/*
|
||||
Slack
|
||||
1. Goto https://api.slack.com/apps/
|
||||
2. Create your app
|
||||
3. On Settings - Basic Information - App Credentials, chose your Signing Secret
|
||||
4. Create a Slash Commands and put as Request URL: https://your.domain.name/slack
|
||||
*/
|
||||
enabled: false,
|
||||
signingSecret: '',
|
||||
},
|
||||
IPLookup: {
|
||||
/*
|
||||
GeoJS
|
||||
https://www.geojs.io/docs/v1/endpoints/geo/
|
||||
*/
|
||||
enabled: false,
|
||||
getEndpoint(ip) {
|
||||
return `https://get.geojs.io/v1/ip/geo/${ip}.json`;
|
||||
},
|
||||
},
|
||||
survey: {
|
||||
/*
|
||||
QuestionPro
|
||||
1. GoTo https://www.questionpro.com/
|
||||
2. Create your account
|
||||
3. Create your custom survey
|
||||
*/
|
||||
enabled: false,
|
||||
url: '',
|
||||
},
|
||||
redirect: {
|
||||
/*
|
||||
Redirect URL on leave room
|
||||
Upon leaving the room, users who either opt out of providing feedback or if the survey is disabled
|
||||
will be redirected to a specified URL. If enabled false the default '/newroom' URL will be used.
|
||||
*/
|
||||
enabled: false,
|
||||
url: '',
|
||||
},
|
||||
ui: {
|
||||
/*
|
||||
Customize your MiroTalk instance
|
||||
*/
|
||||
brand: {
|
||||
app: {
|
||||
name: 'MiroTalk SFU',
|
||||
title: 'MiroTalk SFU<br />Free browser based Real-time video calls.<br />Simple, Secure, Fast.',
|
||||
description:
|
||||
'Start your next video call with a single click. No download, plug-in, or login is required. Just get straight to talking, messaging, and sharing your screen.',
|
||||
},
|
||||
site: {
|
||||
title: 'MiroTalk SFU, Free Video Calls, Messaging and Screen Sharing',
|
||||
icon: '../images/logo.svg',
|
||||
appleTouchIcon: '../images/logo.svg',
|
||||
},
|
||||
meta: {
|
||||
description:
|
||||
'MiroTalk SFU powered by WebRTC and mediasoup, Real-time Simple Secure Fast video calls, messaging and screen sharing capabilities in the browser.',
|
||||
keywords:
|
||||
'webrtc, miro, mediasoup, mediasoup-client, self hosted, voip, sip, real-time communications, chat, messaging, meet, webrtc stun, webrtc turn, webrtc p2p, webrtc sfu, video meeting, video chat, video conference, multi video chat, multi video conference, peer to peer, p2p, sfu, rtc, alternative to, zoom, microsoft teams, google meet, jitsi, meeting',
|
||||
},
|
||||
og: {
|
||||
type: 'app-webrtc',
|
||||
siteName: 'MiroTalk SFU',
|
||||
title: 'Click the link to make a call.',
|
||||
description: 'MiroTalk SFU calling provides real-time video calls, messaging and screen sharing.',
|
||||
image: 'https://sfu.mirotalk.com/images/mirotalksfu.png',
|
||||
},
|
||||
html: {
|
||||
features: true,
|
||||
teams: true, // Please keep me always visible, thank you!
|
||||
tryEasier: true,
|
||||
poweredBy: true,
|
||||
sponsors: true,
|
||||
advertisers: true,
|
||||
footer: true,
|
||||
},
|
||||
//...
|
||||
},
|
||||
/*
|
||||
Toggle the visibility of specific HTML elements within the room
|
||||
*/
|
||||
buttons: {
|
||||
main: {
|
||||
shareButton: true, // presenter
|
||||
hideMeButton: true,
|
||||
startAudioButton: true,
|
||||
startVideoButton: true,
|
||||
startScreenButton: true,
|
||||
swapCameraButton: true,
|
||||
chatButton: true,
|
||||
raiseHandButton: true,
|
||||
transcriptionButton: true,
|
||||
whiteboardButton: true,
|
||||
emojiRoomButton: true,
|
||||
settingsButton: true,
|
||||
aboutButton: true, // Please keep me always visible, thank you!
|
||||
exitButton: true,
|
||||
},
|
||||
settings: {
|
||||
fileSharing: true,
|
||||
lockRoomButton: true, // presenter
|
||||
unlockRoomButton: true, // presenter
|
||||
broadcastingButton: true, // presenter
|
||||
lobbyButton: true, // presenter
|
||||
sendEmailInvitation: true, // presenter
|
||||
micOptionsButton: true, // presenter
|
||||
tabModerator: true, // presenter
|
||||
tabRecording: true,
|
||||
host_only_recording: true, // presenter
|
||||
pushToTalk: true,
|
||||
},
|
||||
producerVideo: {
|
||||
videoPictureInPicture: true,
|
||||
fullScreenButton: true,
|
||||
snapShotButton: true,
|
||||
muteAudioButton: true,
|
||||
videoPrivacyButton: true,
|
||||
},
|
||||
consumerVideo: {
|
||||
videoPictureInPicture: true,
|
||||
fullScreenButton: true,
|
||||
snapShotButton: true,
|
||||
sendMessageButton: true,
|
||||
sendFileButton: true,
|
||||
sendVideoButton: true,
|
||||
muteVideoButton: true,
|
||||
muteAudioButton: true,
|
||||
audioVolumeInput: true, // Disabled for mobile
|
||||
geolocationButton: true, // Presenter
|
||||
banButton: true, // presenter
|
||||
ejectButton: true, // presenter
|
||||
},
|
||||
videoOff: {
|
||||
sendMessageButton: true,
|
||||
sendFileButton: true,
|
||||
sendVideoButton: true,
|
||||
muteAudioButton: true,
|
||||
audioVolumeInput: true, // Disabled for mobile
|
||||
geolocationButton: true, // Presenter
|
||||
banButton: true, // presenter
|
||||
ejectButton: true, // presenter
|
||||
},
|
||||
chat: {
|
||||
chatPinButton: true,
|
||||
chatMaxButton: true,
|
||||
chatSaveButton: true,
|
||||
chatEmojiButton: true,
|
||||
chatMarkdownButton: true,
|
||||
chatSpeechStartButton: true,
|
||||
chatGPT: true,
|
||||
},
|
||||
participantsList: {
|
||||
saveInfoButton: true, // presenter
|
||||
sendFileAllButton: true, // presenter
|
||||
ejectAllButton: true, // presenter
|
||||
sendFileButton: true, // presenter & guests
|
||||
geoLocationButton: true, // presenter
|
||||
banButton: true, // presenter
|
||||
ejectButton: true, // presenter
|
||||
},
|
||||
whiteboard: {
|
||||
whiteboardLockButton: true, // presenter
|
||||
},
|
||||
//...
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
/*
|
||||
Umami: https://github.com/umami-software/umami
|
||||
We use our Self-hosted Umami to track aggregated usage statistics in order to improve our service.
|
||||
*/
|
||||
enabled: true,
|
||||
src: 'https://stats.mirotalk.com/script.js',
|
||||
id: '41d26670-f275-45bb-af82-3ce91fe57756',
|
||||
},
|
||||
mediasoup: {
|
||||
// Worker settings
|
||||
numWorkers: numWorkers,
|
||||
worker: {
|
||||
logLevel: 'error',
|
||||
logTags: ['info', 'ice', 'dtls', 'rtp', 'srtp', 'rtcp', 'rtx', 'bwe', 'score', 'simulcast', 'svc', 'sctp'],
|
||||
},
|
||||
// Router settings
|
||||
router: {
|
||||
mediaCodecs: [
|
||||
{
|
||||
kind: 'audio',
|
||||
mimeType: 'audio/opus',
|
||||
clockRate: 48000,
|
||||
channels: 2,
|
||||
},
|
||||
{
|
||||
kind: 'video',
|
||||
mimeType: 'video/VP8',
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
'x-google-start-bitrate': 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'video',
|
||||
mimeType: 'video/VP9',
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
'profile-id': 2,
|
||||
'x-google-start-bitrate': 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'video',
|
||||
mimeType: 'video/h264',
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
'packetization-mode': 1,
|
||||
'profile-level-id': '4d0032',
|
||||
'level-asymmetry-allowed': 1,
|
||||
'x-google-start-bitrate': 1000,
|
||||
},
|
||||
},
|
||||
{
|
||||
kind: 'video',
|
||||
mimeType: 'video/h264',
|
||||
clockRate: 90000,
|
||||
parameters: {
|
||||
'packetization-mode': 1,
|
||||
'profile-level-id': '42e01f',
|
||||
'level-asymmetry-allowed': 1,
|
||||
'x-google-start-bitrate': 1000,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
// WebRtcServerOptions
|
||||
webRtcServerActive: false,
|
||||
webRtcServerOptions: {
|
||||
listenInfos: [
|
||||
{ protocol: 'udp', ip: '0.0.0.0', announcedAddress: IPv4, port: 44444 },
|
||||
{ protocol: 'tcp', ip: '0.0.0.0', announcedAddress: IPv4, port: 44444 },
|
||||
// { protocol: 'udp', ip: '0.0.0.0', announcedAddress: IPv4, portRange: { min: 44444, max: 44444 + numWorkers }},
|
||||
// { protocol: 'tcp', ip: '0.0.0.0', announcedAddress: IPv4, portRange: { min: 44444, max: 44444 + numWorkers }},
|
||||
],
|
||||
},
|
||||
// WebRtcTransportOptions
|
||||
webRtcTransport: {
|
||||
listenInfos: [
|
||||
{ protocol: 'udp', ip: '0.0.0.0', announcedAddress: IPv4, portRange: { min: 40000, max: 40100 } },
|
||||
{ protocol: 'tcp', ip: '0.0.0.0', announcedAddress: IPv4, portRange: { min: 40000, max: 40100 } },
|
||||
],
|
||||
initialAvailableOutgoingBitrate: 1000000,
|
||||
minimumAvailableOutgoingBitrate: 600000,
|
||||
maxSctpMessageSize: 262144,
|
||||
maxIncomingBitrate: 1500000,
|
||||
},
|
||||
//announcedAddress: replace by 'public static IPV4 address' https://api.ipify.org (type string --> 'xx.xxx.xxx.xx' not xx.xxx.xxx.xx)
|
||||
//announcedAddress: '' will be auto-detected on server start, for docker localPC set '127.0.0.1' otherwise the 'public static IPV4 address'
|
||||
},
|
||||
};
|
150
MiroTalk SFU/app/src/lib/nodemailer.js
Normal file
150
MiroTalk SFU/app/src/lib/nodemailer.js
Normal file
@ -0,0 +1,150 @@
|
||||
'use-strict';
|
||||
|
||||
const nodemailer = require('nodemailer');
|
||||
const config = require('../config');
|
||||
const Logger = require('../Logger');
|
||||
const log = new Logger('NodeMailer');
|
||||
|
||||
// ####################################################
|
||||
// EMAIL CONFIG
|
||||
// ####################################################
|
||||
|
||||
const EMAIL_HOST = config.email ? config.email.host : false;
|
||||
const EMAIL_PORT = config.email ? config.email.port : false;
|
||||
const EMAIL_USERNAME = config.email ? config.email.username : false;
|
||||
const EMAIL_PASSWORD = config.email ? config.email.password : false;
|
||||
const EMAIL_SEND_TO = config.email ? config.email.sendTo : false;
|
||||
const EMAIL_ALERT = config.email ? config.email.alert : false;
|
||||
|
||||
log.info('Email', {
|
||||
alert: EMAIL_ALERT,
|
||||
host: EMAIL_HOST,
|
||||
port: EMAIL_PORT,
|
||||
username: EMAIL_USERNAME,
|
||||
password: EMAIL_PASSWORD,
|
||||
});
|
||||
|
||||
const transport = nodemailer.createTransport({
|
||||
host: EMAIL_HOST,
|
||||
port: EMAIL_PORT,
|
||||
auth: {
|
||||
user: EMAIL_USERNAME,
|
||||
pass: EMAIL_PASSWORD,
|
||||
},
|
||||
});
|
||||
|
||||
// ####################################################
|
||||
// EMAIL SEND ALERTS AND NOTIFICATIONS
|
||||
// ####################################################
|
||||
|
||||
function sendEmailAlert(event, data) {
|
||||
if (!EMAIL_ALERT || !EMAIL_HOST || !EMAIL_PORT || !EMAIL_USERNAME || !EMAIL_PASSWORD || !EMAIL_SEND_TO) return;
|
||||
|
||||
log.info('sendEMailAlert', {
|
||||
event: event,
|
||||
data: data,
|
||||
});
|
||||
|
||||
let subject = false;
|
||||
let body = false;
|
||||
|
||||
switch (event) {
|
||||
case 'join':
|
||||
subject = getJoinRoomSubject(data);
|
||||
body = getJoinRoomBody(data);
|
||||
break;
|
||||
// ...
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (subject && body) sendEmail(subject, body);
|
||||
}
|
||||
|
||||
function sendEmail(subject, body) {
|
||||
transport
|
||||
.sendMail({
|
||||
from: EMAIL_USERNAME,
|
||||
to: EMAIL_SEND_TO,
|
||||
subject: subject,
|
||||
html: body,
|
||||
})
|
||||
.catch((err) => log.error(err));
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// EMAIL TEMPLATES
|
||||
// ####################################################
|
||||
|
||||
function getJoinRoomSubject(data) {
|
||||
const { room_id } = data;
|
||||
return `MiroTalk SFU - New user Join to Room ${room_id}`;
|
||||
}
|
||||
function getJoinRoomBody(data) {
|
||||
const { peer_name, room_id, domain, os, browser } = data;
|
||||
|
||||
const currentDataTime = getCurrentDataTime();
|
||||
|
||||
const localDomains = ['localhost', '127.0.0.1'];
|
||||
|
||||
const currentDomain = localDomains.some((localDomain) => domain.includes(localDomain))
|
||||
? `${domain}:${config.server.listen.port}`
|
||||
: domain;
|
||||
|
||||
const room_join = `https://${currentDomain}/join/`;
|
||||
|
||||
return `
|
||||
<h1>New user join</h1>
|
||||
<style>
|
||||
table {
|
||||
font-family: arial, sans-serif;
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
td {
|
||||
border: 1px solid #dddddd;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
}
|
||||
tr:nth-child(even) {
|
||||
background-color: #dddddd;
|
||||
}
|
||||
</style>
|
||||
<table>
|
||||
<tr>
|
||||
<td>User</td>
|
||||
<td>${peer_name}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Os</td>
|
||||
<td>${os}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Browser</td>
|
||||
<td>${browser}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Room</td>
|
||||
<td>${room_join}${room_id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Date, Time</td>
|
||||
<td>${currentDataTime}</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
// ####################################################
|
||||
// UTILITY
|
||||
// ####################################################
|
||||
|
||||
function getCurrentDataTime() {
|
||||
const currentTime = new Date().toLocaleString('en-US', log.tzOptions);
|
||||
const milliseconds = String(new Date().getMilliseconds()).padStart(3, '0');
|
||||
return `${currentTime}:${milliseconds}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendEmailAlert,
|
||||
};
|
24
MiroTalk SFU/app/src/middleware/IpWhitelist.js
Normal file
24
MiroTalk SFU/app/src/middleware/IpWhitelist.js
Normal file
@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('../config');
|
||||
const Logger = require('../Logger');
|
||||
const log = new Logger('RestrictAccessByIP');
|
||||
|
||||
const IpWhitelistEnabled = config.middleware ? config.middleware.IpWhitelist.enabled : false;
|
||||
const allowedIPs = config.middleware ? config.middleware.IpWhitelist.allowed : [];
|
||||
|
||||
const restrictAccessByIP = (req, res, next) => {
|
||||
if (!IpWhitelistEnabled) return next();
|
||||
//
|
||||
const clientIP =
|
||||
req.headers['x-forwarded-for'] || req.headers['X-Forwarded-For'] || req.socket.remoteAddress || req.ip;
|
||||
log.debug('Check IP', clientIP);
|
||||
if (allowedIPs.includes(clientIP)) {
|
||||
next();
|
||||
} else {
|
||||
log.info('Forbidden: Access denied from this IP address', { clientIP: clientIP });
|
||||
res.status(403).json({ error: 'Forbidden', message: 'Access denied from this IP address.' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = restrictAccessByIP;
|
159
MiroTalk SFU/app/src/scripts/bindable.js
Normal file
159
MiroTalk SFU/app/src/scripts/bindable.js
Normal file
@ -0,0 +1,159 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('../config');
|
||||
|
||||
const net = require('net');
|
||||
|
||||
/*
|
||||
Run: node bindable.js
|
||||
|
||||
In networking, "bindable" refers to the ability to assign or allocate a specific IP address and port combination
|
||||
to a network service or application. Binding an IP address and port allows the service or application to listen for
|
||||
incoming network connections on that particular address and port.
|
||||
|
||||
When we say an IP address and port are "bindable," it means that there are no conflicts or issues preventing the service
|
||||
or application from using that specific combination. In other words, the IP address is available, and the port is not already
|
||||
in use by another process or service on the same machine.
|
||||
|
||||
If an IP address and port are bindable, it indicates that the network service or application can successfully bind to that
|
||||
combination, allowing it to accept incoming connections and communicate over the network. On the other hand, if the IP address
|
||||
and port are not bindable, it suggests that there may be conflicts or restrictions preventing the service or application
|
||||
from using them, such as another process already listening on the same IP address and port.
|
||||
*/
|
||||
|
||||
async function main() {
|
||||
// Server listen
|
||||
const serverListenIp = config.server.listen.ip;
|
||||
const serverListenPort = config.server.listen.port;
|
||||
|
||||
// WebRtcServerActive
|
||||
const webRtcServerActive = config.mediasoup.webRtcServerActive;
|
||||
|
||||
// WebRtcTransportOptions
|
||||
const webRtcTransportIpInfo = config.mediasoup.webRtcTransport.listenInfos[0];
|
||||
const webRtcTransportIpAddress =
|
||||
webRtcTransportIpInfo.ip !== '0.0.0.0' ? webRtcTransportIpInfo.ip : webRtcTransportIpInfo.announcedAddress;
|
||||
|
||||
// WorkersOptions | webRtcTransportOptions
|
||||
const workers = config.mediasoup.numWorkers;
|
||||
const { min, max } = config.mediasoup.webRtcTransport.listenInfos[0].portRange;
|
||||
const rtcMinPort = config.mediasoup.worker.rtcMinPort || min || 40000;
|
||||
const rtcMaxPort = config.mediasoup.worker.rtcMaxPort || max || 40100;
|
||||
|
||||
console.log('==================================');
|
||||
console.log('checkServerListenPorts');
|
||||
console.log('==================================');
|
||||
|
||||
await checkServerListenPorts(serverListenIp, serverListenPort);
|
||||
|
||||
console.log('==================================');
|
||||
console.log('checkWebRtcTransportPorts');
|
||||
console.log('==================================');
|
||||
|
||||
await checkWebRtcTransportPorts(webRtcTransportIpAddress, rtcMinPort, rtcMaxPort);
|
||||
|
||||
if (webRtcServerActive) {
|
||||
console.log('==================================');
|
||||
console.log('checkWebRtcServerPorts');
|
||||
console.log('==================================');
|
||||
|
||||
// WebRtcServerOptions
|
||||
const webRtcServerIpInfo = config.mediasoup.webRtcServerOptions.listenInfos[0];
|
||||
const webRtcServerIpAddress =
|
||||
webRtcServerIpInfo.ip !== '0.0.0.0' ? webRtcServerIpInfo.ip : webRtcServerIpInfo.announcedAddress;
|
||||
const webRtcServerStartPort = webRtcServerIpInfo.port;
|
||||
|
||||
await checkWebRtcServerPorts(webRtcServerIpAddress, webRtcServerStartPort, workers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Server listen port is bindable
|
||||
* @param {string} ipAddress
|
||||
* @param {integer} port
|
||||
*/
|
||||
async function checkServerListenPorts(ipAddress, port) {
|
||||
const bindable = await isBindable(ipAddress, port);
|
||||
if (bindable) {
|
||||
console.log(`${ipAddress}:${port} is bindable 🟢`);
|
||||
} else {
|
||||
console.log(`${ipAddress}:${port} is not bindable 🔴`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WebRtcServer ports are bindable
|
||||
* @param {string} ipAddress
|
||||
* @param {integer} startPort
|
||||
* @param {integer} workers
|
||||
*/
|
||||
async function checkWebRtcServerPorts(ipAddress, startPort, workers) {
|
||||
let port = startPort;
|
||||
for (let i = 0; i < workers; i++) {
|
||||
try {
|
||||
const bindable = await isBindable(ipAddress, port);
|
||||
if (bindable) {
|
||||
console.log(`${ipAddress}:${port} is bindable 🟢`);
|
||||
} else {
|
||||
console.log(`${ipAddress}:${port} is not bindable 🔴`);
|
||||
}
|
||||
port++;
|
||||
} catch (err) {
|
||||
console.error('Error occurred:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WebRtcTransport Worker ports are bindable
|
||||
* @param {string} ipAddress
|
||||
* @param {integer} minPort
|
||||
* @param {integer} maxPort
|
||||
*/
|
||||
async function checkWebRtcTransportPorts(ipAddress, minPort, maxPort) {
|
||||
let port = minPort;
|
||||
for (let i = 0; i <= maxPort - minPort; i++) {
|
||||
try {
|
||||
const bindable = await isBindable(ipAddress, port);
|
||||
if (bindable) {
|
||||
console.log(`${ipAddress}:${port} is bindable 🟢`);
|
||||
} else {
|
||||
console.log(`${ipAddress}:${port} is not bindable 🔴`);
|
||||
}
|
||||
port++;
|
||||
} catch (err) {
|
||||
console.error('Error occurred:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ipAddress:port are bindable
|
||||
* @param {string} ipAddress
|
||||
* @param {integer} port
|
||||
* @returns {Promise<boolean>} A promise that resolves to true if the address is bindable, false otherwise.
|
||||
*/
|
||||
async function isBindable(ipAddress, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = net.createServer();
|
||||
|
||||
server.once('error', (err) => {
|
||||
if (err.code === 'EADDRINUSE') {
|
||||
resolve(false); // Address is already in use
|
||||
} else {
|
||||
reject(err); // Other error occurred
|
||||
}
|
||||
});
|
||||
|
||||
server.once('listening', () => {
|
||||
server.close();
|
||||
resolve(true); // Address is bindable
|
||||
});
|
||||
|
||||
server.listen(port, ipAddress);
|
||||
});
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Error occurred in main function:', err.message);
|
||||
});
|
22
MiroTalk SFU/app/ssl/README.md
Normal file
22
MiroTalk SFU/app/ssl/README.md
Normal file
@ -0,0 +1,22 @@
|
||||
## Self-signed certificate
|
||||
|
||||
[What is self-signed-certificate](https://en.wikipedia.org/wiki/Self-signed_certificate)
|
||||
|
||||

|
||||
|
||||
```bash
|
||||
# install openssl 4 ubuntu
|
||||
apt install openssl
|
||||
# install openssl 4 mac
|
||||
brew install openssl
|
||||
|
||||
# self-signed certificate
|
||||
openssl genrsa -out key.pem
|
||||
openssl req -new -key key.pem -out csr.pem
|
||||
openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
|
||||
rm csr.pem
|
||||
|
||||
# https://www.sslchecker.com/certdecoder
|
||||
```
|
||||
|
||||
For trusted certificate, take a look at [Let's Encrypt](https://letsencrypt.org/i) and [Certbot](https://certbot.eff.org/).
|
22
MiroTalk SFU/app/ssl/cert.pem
Normal file
22
MiroTalk SFU/app/ssl/cert.pem
Normal file
@ -0,0 +1,22 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDlTCCAn0CFBM91H+g2aRKsaRrCmo3NdYjwfWUMA0GCSqGSIb3DQEBCwUAMIGG
|
||||
MQswCQYDVQQGEwJJVDEOMAwGA1UECAwFSXRhbHkxETAPBgNVBAoMCE1pcm9UYWxr
|
||||
MQ8wDQYDVQQLDAZXZWJSVEMxFzAVBgNVBAMMDk1pcm9zbGF2IFBlamljMSowKAYJ
|
||||
KoZIhvcNAQkBFhttaXJvc2xhdi5wZWppYy44NUBnbWFpbC5jb20wHhcNMjEwODEx
|
||||
MTUwOTUzWhcNNDgxMjI2MTUwOTUzWjCBhjELMAkGA1UEBhMCSVQxDjAMBgNVBAgM
|
||||
BUl0YWx5MREwDwYDVQQKDAhNaXJvVGFsazEPMA0GA1UECwwGV2ViUlRDMRcwFQYD
|
||||
VQQDDA5NaXJvc2xhdiBQZWppYzEqMCgGCSqGSIb3DQEJARYbbWlyb3NsYXYucGVq
|
||||
aWMuODVAZ21haWwuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||
rn2tC9W6wqjDI7B/4LfEnE4ILBOdwa9889QjmWUToKua7iXTpaSNP3SefKY50Q8T
|
||||
BFfkZXEGyqAESBUn2rYeHtebgLQTKHsixhSCdHqpBDOyYFeTywGiRP4gQHFKbExd
|
||||
X2AAD1ptTjHVuSlg/ojWstESBh/4TktifKzy3PKVKX6p889eDfyJtlv0PADAkon/
|
||||
rZp3hHq0FORhvQEER1sm6g58WyIfqGWjW7bb7/bkyS1baQI16fPeexfu1Rs7y4kx
|
||||
gX7+9/oA40D3rz0Wf378PTwVzbYCF+hZo/H9yJGTUAZSz84zNbSLvKBZFPfabA2A
|
||||
l92uPgNWoct06uf7ubEJhwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAyH68yi+mf
|
||||
2HNwEscSLN1oU9uXNC4hwHi2xoPrpxoLsbhoc/aVhgd5peOEmIlOQPQofCO/JSKt
|
||||
HqidURSt8U+WrcsXz3HTdq+m/Qw3E/BA5CDXL/LUmIh43cZzkWSawx2EocJr7g1W
|
||||
JeAtUt8xpDtuLlTMjoGGmTsKQG7ub8NcYN7EEqWa2v+2qSTqvhASMs+cf7DT2HZI
|
||||
I3q2v6l1N+DcpO8ey8dbLhbhd4d+bGjyjEcT+clDHFrKsqiYDCS99sOmedmHoyZk
|
||||
+h+CzXICtqFSrAAGE/PoetQtJlojwnu9mJN/xj3i/zJTZTRh3jOGF8Hfg2bvwgdg
|
||||
vMYRLwtknqya
|
||||
-----END CERTIFICATE-----
|
BIN
MiroTalk SFU/app/ssl/https.png
Normal file
BIN
MiroTalk SFU/app/ssl/https.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.2 KiB |
27
MiroTalk SFU/app/ssl/key.pem
Normal file
27
MiroTalk SFU/app/ssl/key.pem
Normal file
@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEArn2tC9W6wqjDI7B/4LfEnE4ILBOdwa9889QjmWUToKua7iXT
|
||||
paSNP3SefKY50Q8TBFfkZXEGyqAESBUn2rYeHtebgLQTKHsixhSCdHqpBDOyYFeT
|
||||
ywGiRP4gQHFKbExdX2AAD1ptTjHVuSlg/ojWstESBh/4TktifKzy3PKVKX6p889e
|
||||
DfyJtlv0PADAkon/rZp3hHq0FORhvQEER1sm6g58WyIfqGWjW7bb7/bkyS1baQI1
|
||||
6fPeexfu1Rs7y4kxgX7+9/oA40D3rz0Wf378PTwVzbYCF+hZo/H9yJGTUAZSz84z
|
||||
NbSLvKBZFPfabA2Al92uPgNWoct06uf7ubEJhwIDAQABAoIBAAhskNota2LSevlS
|
||||
IBpdROS277YRDGC5dDLhXwac1qG/Jy+wK9OnahpSKwShkdECBU0EYUZ0ent11j8U
|
||||
pmPsvu+GQT+pcfNWXotpmhK9iUNmq4nzMHNwlMD3896omYs49JkSLW6QUw6fYU4b
|
||||
LU+ck6D2bwRUrsw433xdbSw1mfXyzyCIWfPNzRmkEcUkCe1RHkGqFv8FrePhezOB
|
||||
tByAwQQLtBt7FMioTSCedOe7B+tuxwqj5Px7Vr8K3x/PKccSId6hS2ihnptYB+n7
|
||||
3eAxoa049wazsW3SXJQrTxUB1z2bh2p2lA0AedgVNme1FCq4zzA7qa2WppcV2ava
|
||||
Gu+dCgECgYEA1xjLFQmiUqCJVqA99DZNvgiPecuHSe2+2QRIk10uUQcxQAF8K9XT
|
||||
ORpO05lc0ccPZt7tIbQytsnt/NxL2mnvXQ+dzzrQCsPu/HH8fLEaJ4xBdxuIFeDU
|
||||
qCBKYlckCQkDwnNYUqCZCNfxb0Csx98RBDYlwDwZa0hFsLPbGen538cCgYEAz6wh
|
||||
iRXZVTqfhy3meWYFmHqeMb0agFugl4d0Lwl/kI/0M62Jc1u1OU+uI6u5fuevEeBB
|
||||
xYjnaDpBuCWccLw6R5luc98QIbxldbx2A07rUGo2JlQafmDX8wI1GVBXgzOw7HAK
|
||||
jHhCw+ZgtvF2c1XjaTPaunKDomPX6Pjt6R23CEECgYEArRRvPbNt4Wz6djElCSC1
|
||||
N+ftg4TJjSx4eGog+CtvvJW8BJPtVdyORZGswknSzZ6O/yj8yTUV5c3g6apegxbh
|
||||
HBIX2wupIjB9WrdiAvgDYrVSbEREIc6zb8Hj+PPDtF2Dn/FurbY6zkntJad2ILKX
|
||||
H7tubxwtHA2gvkpLULPcdDsCgYAxm3WbUHvM7ycCXIWMhEFb7hZx3TFCbiDLcZDg
|
||||
V42AU9LKsW5+/u4oVY9MeA3kcaWRSJeNfyl/7UKboWhgSaZGSjFnPmaVGHLIEA/E
|
||||
tIpjeCudNkPp4mpTYziZ5mYxMhzWLeFnMqcIMrTxnnZkEKU1ESzzkr09AkqmHSh/
|
||||
ohiBwQKBgQCvaDQS7aBS/1+iBrOyMwxARwFirReJHdgEtkBVrB+sasbj1B4Jd3zO
|
||||
gL6FzxKCKBMIF+sSAjKs0XoRG1K6OPGFg1b5naEq023gObO6aBZqSXhHCajGK790
|
||||
Xhrvj6BojWkJUnc8T0cocrwhMJrTFC0u00KgAnRNyNYw1vccd5q2uQ==
|
||||
-----END RSA PRIVATE KEY-----
|
Reference in New Issue
Block a user