Merge branch 'develop' into 'master'
Develop -> Master See merge request knotteye/satyr!7merge-requests/8/merge
commit
e5dfa446a2
|
@ -1,7 +1,6 @@
|
||||||
node_modules
|
node_modules
|
||||||
site/live
|
site/live
|
||||||
config/local.toml
|
config/**/*
|
||||||
config/jwt.pem
|
!config/.gitkeep
|
||||||
config/generated.toml
|
|
||||||
install/db_setup.sql
|
install/db_setup.sql
|
||||||
build/**
|
build/**
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
#DO NOT EDIT THIS FILE
|
|
||||||
#ALL CHANGES SHOULD GO IN LOCAL.TOML
|
|
||||||
[bcrypt]
|
|
||||||
saltRounds = 12
|
|
||||||
|
|
||||||
[satyr]
|
|
||||||
name = ''
|
|
||||||
domain = ''
|
|
||||||
registration = false
|
|
||||||
restrictedNames = ['live']
|
|
||||||
rootredirect = '/users/live'
|
|
||||||
|
|
||||||
[ircd]
|
|
||||||
enable = false
|
|
||||||
port = 6667
|
|
||||||
sid = ''
|
|
||||||
server = ''
|
|
||||||
pass = ''
|
|
||||||
vhost = 'web.satyr.net'
|
|
||||||
|
|
||||||
[database]
|
|
||||||
host = 'localhost'
|
|
||||||
user = 'satyr'
|
|
||||||
password = ''
|
|
||||||
database = 'satyr_db'
|
|
||||||
connectionLimit = '50'
|
|
||||||
connectionTimeout = '1000'
|
|
||||||
insecureAuth = false
|
|
||||||
debug = false
|
|
||||||
|
|
||||||
[server]
|
|
||||||
logs = 0
|
|
||||||
api = false
|
|
||||||
api_user = false
|
|
||||||
api_pass = false
|
|
||||||
|
|
||||||
[server.rtmp]
|
|
||||||
port = 1935
|
|
||||||
chunk_size = 6000
|
|
||||||
gop_cache = true
|
|
||||||
ping = 30
|
|
||||||
ping_timeout = 60
|
|
||||||
|
|
||||||
[server.http]
|
|
||||||
hsts = false
|
|
||||||
directory = './site'
|
|
||||||
port = 8000
|
|
||||||
|
|
||||||
[media]
|
|
||||||
record = false
|
|
||||||
publicEndpoint = 'live'
|
|
||||||
privateEndpoint = 'stream'
|
|
||||||
ffmpeg = ''
|
|
||||||
|
|
||||||
[transcode]
|
|
||||||
adapative = false
|
|
||||||
variants = 3
|
|
||||||
format = 'dash'
|
|
|
@ -1,27 +1,48 @@
|
||||||
## Configuring Satyr
|
## Configuring Satyr
|
||||||
|
|
||||||
### Config file
|
### Config file
|
||||||
All changes to satyr's config will go in the config/local.toml file
|
All changes to satyr's config will go in the config/config.yml file
|
||||||
Some values you might want to change are
|
Some values you might want to change are
|
||||||
```
|
```
|
||||||
[satyr]
|
satyr:
|
||||||
registration = true
|
registration: true
|
||||||
#allow new users to register
|
# allow new users to register
|
||||||
rootRedirect = '/users/live'
|
rootRedirect: '/users/live'
|
||||||
#the page users are directed to when they visit your site root
|
# the page users are directed to when they visit your site root
|
||||||
[media]
|
|
||||||
record = true
|
http:
|
||||||
#allow users to record VODs
|
hsts: true
|
||||||
[bcrypt]
|
# enable strict transport security
|
||||||
saltRounds = 12
|
|
||||||
|
media:
|
||||||
|
record: true
|
||||||
|
# allow users to record VODs
|
||||||
|
|
||||||
|
transcode:
|
||||||
|
adapative: true
|
||||||
|
# enable adaptive livestreaming
|
||||||
|
# will help users with poor connections, but EXTREMELY cpu intensive
|
||||||
|
# even 3 variants will max out most budget VPSs with a single stream
|
||||||
|
variants: 3
|
||||||
|
# the number of adaptive streaming variants to generate
|
||||||
|
# satyr will always copy the source stream
|
||||||
|
# and the remaining variants will lower the quality incrementally
|
||||||
|
|
||||||
|
# So the default setting of 3 will copy the source stream once
|
||||||
|
# And generate two lower quality & bitrate variants
|
||||||
|
|
||||||
|
crypto:
|
||||||
|
saltRounds: 12
|
||||||
#change the number of rounds of bcrypt to fit your hardware
|
#change the number of rounds of bcrypt to fit your hardware
|
||||||
#if you don't understand the implications, don't change this
|
#if you don't understand the implications, don't change this
|
||||||
[ircd]
|
|
||||||
enable = true
|
irc:
|
||||||
#enable IRC peering
|
port: 6667
|
||||||
#unused for now
|
#irc settings
|
||||||
|
#currently unused
|
||||||
```
|
```
|
||||||
|
|
||||||
### Web Frontend
|
### Web Frontend
|
||||||
If you want to customize the front-end css, place a file with any changes you wish to make at site/local.css
|
If you want to customize the front-end css, place a file with any changes you wish to make at site/local.css
|
||||||
You can change the logo by replacing site/logo.svg.
|
You can change the logo by replacing site/logo.svg.
|
||||||
|
You should also consider editing templates/about.html and templates/tos.html
|
|
@ -2,8 +2,8 @@
|
||||||
A more detailed walkthrough.
|
A more detailed walkthrough.
|
||||||
|
|
||||||
### System Dependencies
|
### System Dependencies
|
||||||
Install ffmpeg and mysql through your distribution's package manager.
|
Install ffmpeg(>= 4.2.1) and mysql through your distribution's package manager.
|
||||||
See [this page](https://nodejs.org/en/download/package-manager/) for instructions on install node. Compatible versions are >=10. Nightly builds may fail to compile some of the native addons.
|
See [this page](https://nodejs.org/en/download/package-manager/) for instructions on installing node. Compatible versions are >=10. Nightly builds may fail to compile some of the native addons.
|
||||||
|
|
||||||
### Installing Satyr
|
### Installing Satyr
|
||||||
Clone the repository and change to the directory
|
Clone the repository and change to the directory
|
||||||
|
@ -25,10 +25,9 @@ Run the setup script for the database.
|
||||||
sudo mysql
|
sudo mysql
|
||||||
source install/db_setup.sql;
|
source install/db_setup.sql;
|
||||||
```
|
```
|
||||||
Compile the code and start the server.
|
Then start the server.
|
||||||
```bash
|
```bash
|
||||||
npm run build
|
npm run start
|
||||||
npm start
|
|
||||||
```
|
```
|
||||||
|
|
||||||
It is reccomended that you run Satyr behind a TLS terminating reverse proxy, like nginx.
|
It is reccomended that you run Satyr behind a TLS terminating reverse proxy, like nginx.
|
||||||
|
@ -40,10 +39,10 @@ Updating should be as simple as pulling the latest code and dependencies, then b
|
||||||
```bash
|
```bash
|
||||||
git pull
|
git pull
|
||||||
npm i
|
npm i
|
||||||
npm run build
|
npm update
|
||||||
```
|
```
|
||||||
|
|
||||||
Then restart the server.
|
Then restart the server.
|
||||||
|
|
||||||
## Migrating Satyr
|
## Migrating Satyr
|
||||||
To backup and restore, you will need to export the mysqlDB. Restore the new database from the backup, then copy the config/local.toml file and the site directory to the new install.
|
To backup and restore, you will need to export the mysqlDB. Restore the new database from the backup, then copy config and site directories to the new location.
|
|
@ -52,6 +52,9 @@ The following commands are available:
|
||||||
`/nick kawen (password)` Password is only required if kawen is a registered user.
|
`/nick kawen (password)` Password is only required if kawen is a registered user.
|
||||||
`/join kawen` Join the chatroom for kawen's stream and leave the previous room.
|
`/join kawen` Join the chatroom for kawen's stream and leave the previous room.
|
||||||
`/kick lain` Available only in your own room if you are a streamer. Forcefully disconnect the user.
|
`/kick lain` Available only in your own room if you are a streamer. Forcefully disconnect the user.
|
||||||
|
`/ban lain (time)` Ban a user from your room. Bans are based on IP address. The optional time is in minutes. The default is 30.
|
||||||
|
`/banlist` List the IPs currently banned from your room.
|
||||||
|
`/unban (ip)` self explanatory
|
||||||
|
|
||||||
#### Streaming
|
#### Streaming
|
||||||
Users should stream to rtmp://example.tld/stream/examplestreamkey
|
Users should stream to rtmp://example.tld/stream/examplestreamkey
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
satyr:
|
||||||
|
name: '<iname>'
|
||||||
|
domain: '<domain>'
|
||||||
|
email: '<email>'
|
||||||
|
registration: false
|
||||||
|
|
||||||
|
media:
|
||||||
|
record: false
|
||||||
|
ffmpeg: '<ffmpeg>'
|
||||||
|
|
||||||
|
http:
|
||||||
|
# uncomment to set HSTS when SSL is ready
|
||||||
|
#hsts: true
|
||||||
|
|
||||||
|
database:
|
||||||
|
user: '<dbuser>'
|
||||||
|
password: '<dbpass>'
|
||||||
|
database: '<dbname>'
|
||||||
|
host: '<dbhost>'
|
||||||
|
|
||||||
|
transcode:
|
||||||
|
adaptive: false
|
||||||
|
format: dash
|
||||||
|
variants: 3
|
|
@ -38,8 +38,8 @@ dbclient="${dbclient:='*'}"
|
||||||
else
|
else
|
||||||
dbclient="localhost"
|
dbclient="localhost"
|
||||||
fi
|
fi
|
||||||
sed -e "s#<iname>#$name#g" -e "s#<domain>#$domain#g" -e "s#<ffmpeg>#$ffmpeg#g" -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<email>#$email#g" install/template.local.toml > config/generated.toml
|
sed -e "s#<iname>#$name#g" -e "s#<domain>#$domain#g" -e "s#<ffmpeg>#$ffmpeg#g" -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<email>#$email#g" install/config.example.yml > config/generated.yml
|
||||||
sed -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<dbclient>#$dbclient#g" install/db_template.sql > install/db_setup.sql
|
sed -e "s#<dbuser>#$dbuser#g" -e "s#<dbname>#$dbname#g" -e "s#<dbpass>#$dbpass#g" -e "s#<dbhost>#$dbhost#g" -e "s#<dbclient>#$dbclient#g" install/db_template.sql > install/db_setup.sql
|
||||||
echo "A setup script for the database has been generated at install/db_setup.sql. Please run it by connecting to your database software and executing 'source install/db_setup.sql;''"
|
echo "A setup script for the database has been generated at install/db_setup.sql. Please run it by connecting to your database software and executing 'source install/db_setup.sql;''"
|
||||||
echo "A default configuration file has been generated at config/generated.toml"
|
echo "A default configuration file has been generated at config/generated.yml"
|
||||||
echo "If everything looks fine, move it to config/local.toml and start your instance."
|
echo "If everything looks fine, move it to config/config.yml and start your instance."
|
|
@ -1,19 +0,0 @@
|
||||||
[satyr]
|
|
||||||
name = '<iname>'
|
|
||||||
domain = '<domain>'
|
|
||||||
email = '<email>'
|
|
||||||
registration = false
|
|
||||||
|
|
||||||
[media]
|
|
||||||
record = false
|
|
||||||
ffmpeg = '<ffmpeg>'
|
|
||||||
|
|
||||||
[server.http]
|
|
||||||
# uncomment to set HSTS when SSL is enabled
|
|
||||||
# hsts = true
|
|
||||||
|
|
||||||
[database]
|
|
||||||
user = '<dbuser>'
|
|
||||||
password = '<dbpass>'
|
|
||||||
database = '<dbname>'
|
|
||||||
host = '<dbhost>'
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "satyr",
|
"name": "satyr",
|
||||||
"version": "0.4.4",
|
"version": "0.5.3",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -82,6 +82,11 @@
|
||||||
"readable-stream": "^2.0.6"
|
"readable-stream": "^2.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"arg": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg=="
|
||||||
|
},
|
||||||
"arr-diff": {
|
"arr-diff": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
|
||||||
|
@ -342,6 +347,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"buffer-from": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
|
||||||
|
},
|
||||||
"bytes": {
|
"bytes": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
|
||||||
|
@ -644,6 +654,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
"integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups="
|
||||||
},
|
},
|
||||||
|
"diff": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q=="
|
||||||
|
},
|
||||||
"dirty": {
|
"dirty": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/dirty/-/dirty-1.1.0.tgz",
|
||||||
|
@ -1910,6 +1925,11 @@
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
|
||||||
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
|
||||||
},
|
},
|
||||||
|
"make-error": {
|
||||||
|
"version": "1.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
|
||||||
|
"integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g=="
|
||||||
|
},
|
||||||
"map-cache": {
|
"map-cache": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
|
||||||
|
@ -2043,6 +2063,11 @@
|
||||||
"minimist": "0.0.8"
|
"minimist": "0.0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"moment": {
|
||||||
|
"version": "2.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
|
||||||
|
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg=="
|
||||||
|
},
|
||||||
"ms": {
|
"ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
|
@ -2287,6 +2312,11 @@
|
||||||
"os-tmpdir": "^1.0.0"
|
"os-tmpdir": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"parse-yaml": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-yaml/-/parse-yaml-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-tLfs2QiziUPFTA4nNrv2rrC0CnHDIF2o2m5TCgNss/E0asI0ltVjBcNKhcd/8vteZa8xKV5RGfD0ZFFlECMCqQ=="
|
||||||
|
},
|
||||||
"parseqs": {
|
"parseqs": {
|
||||||
"version": "0.0.5",
|
"version": "0.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.5.tgz",
|
||||||
|
@ -2712,6 +2742,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"socket-anti-spam": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket-anti-spam/-/socket-anti-spam-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-glCDT8LrqwSY+tQJtvaz3YwTw1HL6bgWVvaQFumkClOcF+Jbg0NlAImqQabowNJcrCxr1dibKRoAvIfN98FKVw==",
|
||||||
|
"requires": {
|
||||||
|
"moment": "^2.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"socket.io": {
|
"socket.io": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.3.0.tgz",
|
||||||
|
@ -2855,6 +2893,22 @@
|
||||||
"urix": "^0.1.0"
|
"urix": "^0.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"source-map-support": {
|
||||||
|
"version": "0.5.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz",
|
||||||
|
"integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==",
|
||||||
|
"requires": {
|
||||||
|
"buffer-from": "^1.0.0",
|
||||||
|
"source-map": "^0.6.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"source-map-url": {
|
"source-map-url": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz",
|
||||||
|
@ -3016,6 +3070,18 @@
|
||||||
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
|
||||||
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
|
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="
|
||||||
},
|
},
|
||||||
|
"ts-node": {
|
||||||
|
"version": "8.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.5.4.tgz",
|
||||||
|
"integrity": "sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw==",
|
||||||
|
"requires": {
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"source-map-support": "^0.5.6",
|
||||||
|
"yn": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"type-is": {
|
"type-is": {
|
||||||
"version": "1.6.18",
|
"version": "1.6.18",
|
||||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
@ -3188,6 +3254,11 @@
|
||||||
"version": "0.1.2",
|
"version": "0.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
|
||||||
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
|
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
|
||||||
|
},
|
||||||
|
"yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,8 +5,8 @@
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"author": "knotteye",
|
"author": "knotteye",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "tsc && node build/controller.js",
|
"start": "ts-node src/index.ts",
|
||||||
"user": "node build/cli.js",
|
"user": "ts-node src/cli.ts",
|
||||||
"setup": "sh install/setup.sh"
|
"setup": "sh install/setup.sh"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -16,7 +16,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bcrypt": "^3.0.6",
|
"bcrypt": "^3.0.6",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"config": "^3.2.2",
|
|
||||||
"cookie-parser": "^1.4.4",
|
"cookie-parser": "^1.4.4",
|
||||||
"dirty": "^1.1.0",
|
"dirty": "^1.1.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
@ -25,10 +24,12 @@
|
||||||
"mysql": "^2.17.1",
|
"mysql": "^2.17.1",
|
||||||
"node-media-server": ">=2.1.3 <3.0.0",
|
"node-media-server": ">=2.1.3 <3.0.0",
|
||||||
"nunjucks": "^3.2.0",
|
"nunjucks": "^3.2.0",
|
||||||
|
"parse-yaml": "^0.1.0",
|
||||||
"recursive-readdir": "^2.2.2",
|
"recursive-readdir": "^2.2.2",
|
||||||
|
"socket-anti-spam": "^2.0.0",
|
||||||
"socket.io": "^2.3.0",
|
"socket.io": "^2.3.0",
|
||||||
"strftime": "^0.10.0",
|
"strftime": "^0.10.0",
|
||||||
"toml": "^3.0.0",
|
"ts-node": "^8.5.4",
|
||||||
"typescript": "^3.6.3"
|
"typescript": "^3.6.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
15
src/api.ts
15
src/api.ts
|
@ -1,17 +1,12 @@
|
||||||
import * as db from "./database"
|
import * as db from "./database"
|
||||||
import { unregisterUser } from "./irc";
|
import { config } from "./config";
|
||||||
|
|
||||||
var config: any;
|
|
||||||
function init(conf: object){
|
|
||||||
config = conf;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function register(name: string, password: string, confirm: string) {
|
async function register(name: string, password: string, confirm: string) {
|
||||||
if(!config.registration) return {"error":"registration disabled"};
|
if(!config['satyr']['registration']) return {"error":"registration disabled"};
|
||||||
if(name.includes(';') || name.includes(' ') || name.includes('\'')) return {"error":"illegal characters"};
|
if(name.includes(';') || name.includes(' ') || name.includes('\'')) return {"error":"illegal characters"};
|
||||||
if(password !== confirm) return {"error":"mismatched passwords"};
|
if(password !== confirm) return {"error":"mismatched passwords"};
|
||||||
for(let i=0;i<config.restrictedNames.length;i++){
|
for(let i=0;i<config['satyr']['restrictedNames'].length;i++){
|
||||||
if (name === config.restrictedNames[i]) return {"error":"restricted name"};
|
if (name === config['satyr']['restrictedNames'][i]) return {"error":"restricted name"};
|
||||||
}
|
}
|
||||||
let r: boolean = await db.addUser(name, password);
|
let r: boolean = await db.addUser(name, password);
|
||||||
if(r) {
|
if(r) {
|
||||||
|
@ -61,4 +56,4 @@ async function login(name: string, password: string){
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { init, register, update, changepwd, changesk, login };
|
export { register, update, changepwd, changesk, login };
|
|
@ -1,8 +1,7 @@
|
||||||
import * as db from "./database"
|
import * as db from "./database"
|
||||||
import * as flags from "flags";
|
import * as flags from "flags";
|
||||||
import * as config from "config"
|
|
||||||
|
|
||||||
db.init(config.database, config.bcrypt);
|
db.init();
|
||||||
|
|
||||||
flags.defineString('adduser', '', 'User to add');
|
flags.defineString('adduser', '', 'User to add');
|
||||||
flags.defineString('rmuser', '', 'User to remove');
|
flags.defineString('rmuser', '', 'User to remove');
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import {parseAsYaml as parse} from "parse-yaml";
|
||||||
|
import {readFileSync as read} from "fs";
|
||||||
|
var localconfig: Object = parse(read('config/config.yml'));
|
||||||
|
const config: Object = {
|
||||||
|
crypto: Object.assign({
|
||||||
|
saltRounds: 12
|
||||||
|
}, localconfig['crypto']),
|
||||||
|
satyr: Object.assign({
|
||||||
|
name: '',
|
||||||
|
domain: '',
|
||||||
|
registration: false,
|
||||||
|
restrictedNames: [ 'live' ],
|
||||||
|
rootredirect: '/users/live',
|
||||||
|
version: process.env.npm_package_version,
|
||||||
|
}, localconfig['satyr']),
|
||||||
|
ircd: Object.assign({
|
||||||
|
port: 6667,
|
||||||
|
}, localconfig['ircd']),
|
||||||
|
database: Object.assign({
|
||||||
|
host: 'localhost',
|
||||||
|
user: 'satyr',
|
||||||
|
password: '',
|
||||||
|
database: 'satyr_db',
|
||||||
|
connectionLimit: '50',
|
||||||
|
connectionTimeout: '1000',
|
||||||
|
insecureAuth: false,
|
||||||
|
debug: false }, localconfig['database']),
|
||||||
|
rtmp: Object.assign({
|
||||||
|
port: 1935,
|
||||||
|
chunk_size: 6000,
|
||||||
|
gop_cache: true,
|
||||||
|
ping: 30,
|
||||||
|
ping_timeout: 60 }, localconfig['rtmp']),
|
||||||
|
http: Object.assign({
|
||||||
|
hsts: false, directory: './site', port: 8000
|
||||||
|
}, localconfig['http']),
|
||||||
|
media: Object.assign({
|
||||||
|
record: false,
|
||||||
|
publicEndpoint: 'live',
|
||||||
|
privateEndpoint: 'stream',
|
||||||
|
ffmpeg: ''
|
||||||
|
}, localconfig['media']),
|
||||||
|
transcode: Object.assign({
|
||||||
|
adapative: false,
|
||||||
|
variants: 3,
|
||||||
|
format: 'dash'
|
||||||
|
}, localconfig['transcode'])
|
||||||
|
};
|
||||||
|
export { config };
|
|
@ -1,67 +0,0 @@
|
||||||
import * as mediaserver from "./server";
|
|
||||||
import * as db from "./database";
|
|
||||||
import * as api from "./api";
|
|
||||||
import * as http from "./http";
|
|
||||||
import * as cleanup from "./cleanup";
|
|
||||||
import * as config from "config";
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
const dbcfg: object = config.database;
|
|
||||||
const bcryptcfg: object = config.bcrypt;
|
|
||||||
const satyr: object = {
|
|
||||||
privateEndpoint: config.media.privateEndpoint,
|
|
||||||
publicEndpoint: config.media.publicEndpoint,
|
|
||||||
record: config.media.record,
|
|
||||||
registration: config.satyr.registration,
|
|
||||||
webFormat: config.satyr.webFormat,
|
|
||||||
restrictedNames: config.satyr.restrictedNames,
|
|
||||||
name: config.satyr.name,
|
|
||||||
domain: config.satyr.domain,
|
|
||||||
email: config.satyr.email,
|
|
||||||
rootredirect: config.satyr.rootredirect,
|
|
||||||
version: process.env.npm_package_version,
|
|
||||||
directory: config.server.http.directory,
|
|
||||||
ffmpeg: config.media.ffmpeg
|
|
||||||
};
|
|
||||||
const nms: object = {
|
|
||||||
logType: config.server.logs,
|
|
||||||
rtmp: {
|
|
||||||
port: config.server.rtmp.port,
|
|
||||||
chunk_size: config.server.rtmp.chunk_size,
|
|
||||||
gop_cache: config.server.rtmp.gop_cache,
|
|
||||||
ping: config.server.rtmp.ping,
|
|
||||||
ping_timeout: config.server.rtmp.ping_timeout,
|
|
||||||
},
|
|
||||||
/*http: {
|
|
||||||
port: config.server.http.port + 1,
|
|
||||||
mediaroot: config.server.http.directory,
|
|
||||||
allow_origin: config.server.http.allow_origin
|
|
||||||
},
|
|
||||||
trans: {
|
|
||||||
ffmpeg: config.media.ffmpeg,
|
|
||||||
tasks: [
|
|
||||||
{
|
|
||||||
app: config.media.publicEndpoint,
|
|
||||||
hls: config.transcode.hls,
|
|
||||||
hlsFlags: config.transcode.hlsFlags,
|
|
||||||
dash: config.transcode.dash,
|
|
||||||
dashFlags: config.transcode.dashFlags
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},*/
|
|
||||||
auth: {
|
|
||||||
api: config.server.api,
|
|
||||||
api_user: config.server.api_user,
|
|
||||||
api_pass: config.server.api_pass
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
|
||||||
db.init(dbcfg, bcryptcfg);
|
|
||||||
await cleanup.init();
|
|
||||||
api.init(satyr);
|
|
||||||
http.init(satyr, config.server.http, config.ircd);
|
|
||||||
mediaserver.init(nms, satyr);
|
|
||||||
console.log(`Satyr v${process.env.npm_package_version} ready`);
|
|
||||||
}
|
|
||||||
run();
|
|
||||||
export { run };
|
|
|
@ -1,20 +1,21 @@
|
||||||
import * as mysql from "mysql";
|
import * as mysql from "mysql";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
import * as crypto from "crypto";
|
import * as crypto from "crypto";
|
||||||
|
import { config } from "./config";
|
||||||
import { resolve } from "url";
|
import { resolve } from "url";
|
||||||
var raw: any;
|
var raw;
|
||||||
var cryptoconfig: any;
|
var cryptoconfig: Object;
|
||||||
|
|
||||||
function init (db: object, bcrypt: object){
|
function init (){
|
||||||
raw = mysql.createPool(db);
|
raw = mysql.createPool(config['database']);
|
||||||
cryptoconfig = bcrypt;
|
cryptoconfig = config['crypto'];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addUser(name: string, password: string){
|
async function addUser(name: string, password: string){
|
||||||
//does not respect registration setting in config
|
//does not respect registration setting in config
|
||||||
if(password === '') return false;
|
if(password === '') return false;
|
||||||
let key: string = await genKey();
|
let key: string = await genKey();
|
||||||
let hash: string = await bcrypt.hash(password, cryptoconfig.saltRounds);
|
let hash: string = await bcrypt.hash(password, cryptoconfig['saltRounds']);
|
||||||
let dupe = await query('select * from users where username='+raw.escape(name));
|
let dupe = await query('select * from users where username='+raw.escape(name));
|
||||||
if(dupe[0]) return false;
|
if(dupe[0]) return false;
|
||||||
await query('INSERT INTO users (username, password_hash, stream_key, record_flag) VALUES ('+raw.escape(name)+', '+raw.escape(hash)+', '+raw.escape(key)+', 0)');
|
await query('INSERT INTO users (username, password_hash, stream_key, record_flag) VALUES ('+raw.escape(name)+', '+raw.escape(hash)+', '+raw.escape(key)+', 0)');
|
||||||
|
@ -54,7 +55,7 @@ async function validatePassword(username: string, password: string){
|
||||||
}
|
}
|
||||||
|
|
||||||
async function hash(pwd){
|
async function hash(pwd){
|
||||||
return await bcrypt.hash(pwd, cryptoconfig.saltRounds);
|
return await bcrypt.hash(pwd, cryptoconfig['saltRounds']);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { query, raw, init, addUser, rmUser, validatePassword, hash, genKey };
|
export { query, raw, init, addUser, rmUser, validatePassword, hash, genKey };
|
171
src/http.ts
171
src/http.ts
|
@ -5,9 +5,10 @@ import * as socketio from "socket.io";
|
||||||
import * as http from "http";
|
import * as http from "http";
|
||||||
import * as cookies from "cookie-parser";
|
import * as cookies from "cookie-parser";
|
||||||
import * as dirty from "dirty";
|
import * as dirty from "dirty";
|
||||||
|
import * as socketSpam from "socket-anti-spam";
|
||||||
import * as api from "./api";
|
import * as api from "./api";
|
||||||
import * as db from "./database";
|
import * as db from "./database";
|
||||||
import * as irc from "./irc";
|
import { config } from "./config";
|
||||||
import { readdir, readFileSync, writeFileSync } from "fs";
|
import { readdir, readFileSync, writeFileSync } from "fs";
|
||||||
import { JWT, JWK } from "jose";
|
import { JWT, JWK } from "jose";
|
||||||
import { strict } from "assert";
|
import { strict } from "assert";
|
||||||
|
@ -18,9 +19,11 @@ const app = express();
|
||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const io = socketio(server);
|
const io = socketio(server);
|
||||||
const store = dirty();
|
const store = dirty();
|
||||||
|
var banlist;
|
||||||
var jwkey;
|
var jwkey;
|
||||||
try{
|
try{
|
||||||
jwkey = JWK.asKey(readFileSync('./config/jwt.pem'));
|
jwkey = JWK.asKey(readFileSync('./config/jwt.pem'));
|
||||||
|
console.log('Found key for JWT signing.');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("No key found for JWT signing, generating one now.");
|
console.log("No key found for JWT signing, generating one now.");
|
||||||
jwkey = JWK.generateSync('RSA', 2048, { use: 'sig' });
|
jwkey = JWK.generateSync('RSA', 2048, { use: 'sig' });
|
||||||
|
@ -28,23 +31,23 @@ try{
|
||||||
}
|
}
|
||||||
var njkconf;
|
var njkconf;
|
||||||
|
|
||||||
async function init(satyr: any, http: object, ircconf: any){
|
async function init(){
|
||||||
njk.configure('templates', {
|
njk.configure('templates', {
|
||||||
autoescape : true,
|
autoescape : true,
|
||||||
express : app,
|
express : app,
|
||||||
watch : false
|
watch : false
|
||||||
});
|
});
|
||||||
njkconf ={
|
njkconf = {
|
||||||
sitename: satyr.name,
|
sitename: config['satyr']['name'],
|
||||||
domain: satyr.domain,
|
domain: config['satyr']['domain'],
|
||||||
email: satyr.email,
|
email: config['satyr']['email'],
|
||||||
rootredirect: satyr.rootredirect,
|
rootredirect: config['satyr']['rootredirect'],
|
||||||
version: satyr.version
|
version: config['satyr']['version']
|
||||||
};
|
};
|
||||||
app.use(cookies());
|
app.use(cookies());
|
||||||
app.use(bodyparser.json());
|
app.use(bodyparser.json());
|
||||||
app.use(bodyparser.urlencoded({ extended: true }));
|
app.use(bodyparser.urlencoded({ extended: true }));
|
||||||
if(http['hsts']){
|
if(config['http']['hsts']){
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
res.append('Strict-Transport-Security', 'max-age=5184000');
|
res.append('Strict-Transport-Security', 'max-age=5184000');
|
||||||
next();
|
next();
|
||||||
|
@ -52,11 +55,11 @@ async function init(satyr: any, http: object, ircconf: any){
|
||||||
}
|
}
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
//site handlers
|
//site handlers
|
||||||
await initSite(satyr.registration);
|
await initSite(config['satyr']['registration']);
|
||||||
//api handlers
|
//api handlers
|
||||||
await initAPI();
|
await initAPI();
|
||||||
//static files if nothing else matches first
|
//static files if nothing else matches first
|
||||||
app.use(express.static(satyr.directory));
|
app.use(express.static(config['http']['directory']));
|
||||||
//404 Handler
|
//404 Handler
|
||||||
app.use(function (req, res, next) {
|
app.use(function (req, res, next) {
|
||||||
if(tryDecode(req.cookies.Authorization)) {
|
if(tryDecode(req.cookies.Authorization)) {
|
||||||
|
@ -65,19 +68,22 @@ async function init(satyr: any, http: object, ircconf: any){
|
||||||
else res.status(404).render('404.njk', njkconf);
|
else res.status(404).render('404.njk', njkconf);
|
||||||
//res.status(404).render('404.njk', njkconf);
|
//res.status(404).render('404.njk', njkconf);
|
||||||
});
|
});
|
||||||
await initChat(ircconf);
|
banlist = new dirty('./config/bans.db').on('load', () => {initChat()});
|
||||||
server.listen(http['port']);
|
server.listen(config['http']['port']);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function newNick(socket, skip?: boolean) {
|
async function newNick(socket, skip?: boolean, i?: number) {
|
||||||
if(socket.handshake.headers['cookie'] && !skip){
|
if(socket.handshake.headers['cookie'] && !skip){
|
||||||
let c = await parseCookie(socket.handshake.headers['cookie']);
|
let c = await parseCookie(socket.handshake.headers['cookie']);
|
||||||
let t = await validToken(c['Authorization']);
|
let t = await validToken(c['Authorization']);
|
||||||
if(t) return t['username'];
|
if(t) {
|
||||||
|
store.set(t['username'], [].concat(store.get(t['username']), socket.id).filter(item => item !== undefined));
|
||||||
|
return t['username'];
|
||||||
}
|
}
|
||||||
//i just realized how shitty of an idea this is
|
}
|
||||||
let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(1000));
|
if(!i) i = 10;
|
||||||
if(store.get(n)) return newNick(socket, true);
|
let n: string = 'Guest'+Math.floor(Math.random() * Math.floor(i));
|
||||||
|
if(store.get(n)) return newNick(socket, true, Math.floor(i * 10));
|
||||||
else {
|
else {
|
||||||
store.set(n, socket.id);
|
store.set(n, socket.id);
|
||||||
return n;
|
return n;
|
||||||
|
@ -89,8 +95,12 @@ async function chgNick(socket, nick, f?: boolean) {
|
||||||
for(let i=1;i<rooms.length;i++){
|
for(let i=1;i<rooms.length;i++){
|
||||||
io.to(rooms[i]).emit('ALERT', socket.nick+' is now known as '+nick);
|
io.to(rooms[i]).emit('ALERT', socket.nick+' is now known as '+nick);
|
||||||
}
|
}
|
||||||
if(store.get(socket.nick)) store.rm(socket.nick);
|
if(store.get(socket.nick)) {
|
||||||
if (!f) store.set(nick, socket.id);
|
if(Array.isArray(store.get(socket.nick))) store.set(socket.nick, store.get(socket.nick).filter(item => item !== socket.id));
|
||||||
|
else store.rm(socket.nick);
|
||||||
|
}
|
||||||
|
if(f) store.set(nick, [].concat(store.get(nick), [socket.id]).filter(item => item !== undefined));
|
||||||
|
else store.set(nick, socket.id);
|
||||||
socket.nick = nick;
|
socket.nick = nick;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,30 +338,26 @@ async function initSite(openReg) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function initChat(ircconf: any) {
|
async function initChat() {
|
||||||
//irc peering
|
//set a cookie to request same nick
|
||||||
if(ircconf.enable){
|
|
||||||
await irc.connect({
|
|
||||||
port: ircconf.port,
|
|
||||||
sid: ircconf.sid,
|
|
||||||
pass: ircconf.pass,
|
|
||||||
server: ircconf.server,
|
|
||||||
vhost: ircconf.vhost
|
|
||||||
});
|
|
||||||
irc.events.on('message', (nick, channel, msg) => {
|
|
||||||
io.to(channel).emit('MSG', {nick: nick, msg: msg});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//socket.io chat logic
|
//socket.io chat logic
|
||||||
io.on('connection', async (socket) => {
|
io.on('connection', async (socket) => {
|
||||||
socket.nick = await newNick(socket);
|
socket.nick = await newNick(socket);
|
||||||
if(ircconf.enable) irc.registerUser(socket.nick);
|
|
||||||
socket.on('JOINROOM', async (data) => {
|
socket.on('JOINROOM', async (data) => {
|
||||||
let t: any = await db.query('select username from users where username='+db.raw.escape(data));
|
let t: any = await db.query('select username from users where username='+db.raw.escape(data));
|
||||||
if(t[0]){
|
if(t[0]){
|
||||||
|
if(banlist.get(data) && banlist.get(data)[socket['handshake']['address']]){
|
||||||
|
if(Math.floor(banlist.get(data)[socket['handshake']['address']]['time'] + (banlist.get(data)[socket['handshake']['address']]['length'] * 60)) < Math.floor(Date.now() / 1000)){
|
||||||
|
banlist.set(data, Object.assign({}, banlist.get(data), {[socket['handshake']['address']]: null}));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
socket.emit('ALERT', 'You are banned from that room');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
socket.join(data);
|
socket.join(data);
|
||||||
io.to(data).emit('JOINED', {nick: socket.nick});
|
io.to(data).emit('JOINED', {nick: socket.nick});
|
||||||
if(ircconf.enable) irc.join(socket.nick, data);
|
|
||||||
}
|
}
|
||||||
else socket.emit('ALERT', 'Room does not exist');
|
else socket.emit('ALERT', 'Room does not exist');
|
||||||
});
|
});
|
||||||
|
@ -372,24 +378,22 @@ async function initChat(ircconf: any) {
|
||||||
});
|
});
|
||||||
socket.on('LEAVEROOM', (data) => {
|
socket.on('LEAVEROOM', (data) => {
|
||||||
socket.leave(data);
|
socket.leave(data);
|
||||||
if(ircconf.enable) irc.part(socket.nick, data);
|
|
||||||
io.to(data).emit('LEFT', {nick: socket.nick});
|
io.to(data).emit('LEFT', {nick: socket.nick});
|
||||||
});
|
});
|
||||||
socket.on('disconnecting', (reason) => {
|
socket.on('disconnecting', (reason) => {
|
||||||
let rooms = Object.keys(socket.rooms);
|
let rooms = Object.keys(socket.rooms);
|
||||||
for(let i=1;i<rooms.length;i++){
|
for(let i=1;i<rooms.length;i++){
|
||||||
if(ircconf.enable) irc.part(socket.nick, rooms[i]);
|
|
||||||
io.to(rooms[i]).emit('ALERT', socket.nick+' disconnected');
|
io.to(rooms[i]).emit('ALERT', socket.nick+' disconnected');
|
||||||
}
|
}
|
||||||
if(ircconf.enable) irc.unregisterUser(socket.nick);
|
if(Array.isArray(store.get(socket.nick))) {
|
||||||
|
store.set(socket.nick, store.get(socket.nick).filter(item => item !== socket.id))
|
||||||
|
if(store.get(socket.nick) !== [])
|
||||||
|
return;
|
||||||
|
}
|
||||||
store.rm(socket.nick);
|
store.rm(socket.nick);
|
||||||
});
|
});
|
||||||
socket.on('NICK', async (data) => {
|
socket.on('NICK', async (data) => {
|
||||||
data.nick = data.nick.replace(' ','');
|
data.nick = data.nick.replace(' ','');
|
||||||
if(store.get(data.nick)){
|
|
||||||
socket.emit('ALERT', 'Nickname is already in use');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
let user = await db.query('select username from users where username='+db.raw.escape(data.nick));
|
let user = await db.query('select username from users where username='+db.raw.escape(data.nick));
|
||||||
if(user[0]){
|
if(user[0]){
|
||||||
if(!data.password){
|
if(!data.password){
|
||||||
|
@ -402,27 +406,102 @@ async function initChat(ircconf: any) {
|
||||||
else socket.emit('ALERT','Incorrect username or password');
|
else socket.emit('ALERT','Incorrect username or password');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
if(store.get(data.nick)){
|
||||||
|
socket.emit('ALERT', 'Nickname is already in use');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
chgNick(socket, data.nick);
|
chgNick(socket, data.nick);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
socket.on('MSG', (data) => {
|
socket.on('MSG', (data) => {
|
||||||
if(data.msg === "" || !data.msg.replace(/\s/g, '').length) return;
|
if(data.msg === "" || !data.msg.replace(/\s/g, '').length) return;
|
||||||
io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg});
|
if(socket.rooms[data['room']]) io.to(data.room).emit('MSG', {nick: socket.nick, msg: data.msg});
|
||||||
if(ircconf.enable) irc.send(socket.nick, data.room, data.msg);
|
|
||||||
});
|
});
|
||||||
socket.on('KICK', (data) => {
|
socket.on('KICK', (data) => {
|
||||||
if(socket.nick === data.room){
|
if(socket.nick === data.room){
|
||||||
//find client with data.nick
|
//find client with data.nick
|
||||||
let id: string = store.get(data.nick);
|
let id: string = store.get(data.nick);
|
||||||
if(id){
|
if(id){
|
||||||
|
if(Array.isArray(id)) {
|
||||||
|
for(let i=0;i<id.length;i++){
|
||||||
|
io.sockets.connected[id[i]].leave(data.room);
|
||||||
|
}
|
||||||
|
io.in(data.room).emit('ALERT', data.nick+' has been kicked.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
let target = io.sockets.connected[id];
|
let target = io.sockets.connected[id];
|
||||||
io.in(data.room).emit('ALERT', data.nick+' has been kicked.');
|
io.in(data.room).emit('ALERT', data.nick+' has been kicked.');
|
||||||
target.disconnect(true);
|
target.leave(data.room);
|
||||||
}
|
}
|
||||||
else socket.emit('ALERT', 'No such user found.');
|
else socket.emit('ALERT', 'No such user found.');
|
||||||
}
|
}
|
||||||
else socket.emit('ALERT', 'Not authorized to do that.');
|
else socket.emit('ALERT', 'Not authorized to do that.');
|
||||||
});
|
});
|
||||||
|
socket.on('BAN', (data: Object) => {
|
||||||
|
if(socket.nick === data['room']){
|
||||||
|
let id: string = store.get(data['nick']);
|
||||||
|
if(id){
|
||||||
|
if(Array.isArray(id)) {
|
||||||
|
for(let i=0;i<id.length;i++){
|
||||||
|
let target = io.sockets.connected[id[i]];
|
||||||
|
if(typeof(data['time']) === 'number' && (data['time'] !== 0 && data['time'] !== NaN)) banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: data['time']}}));
|
||||||
|
else banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: 30}}));
|
||||||
|
target.leave(data['room']);
|
||||||
|
}
|
||||||
|
io.to(data['room']).emit('ALERT', data['nick']+' was banned.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let target = io.sockets.connected[id];
|
||||||
|
if(typeof(data['time']) === 'number' && (data['time'] !== 0 && data['time'] !== NaN)) banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: data['time']}}));
|
||||||
|
else banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[target.ip]: {time: Math.floor(Date.now() / 1000), length: 30}}));
|
||||||
|
target.leave(data['room']);
|
||||||
|
io.to(data['room']).emit('ALERT', target.nick+' was banned.');
|
||||||
|
}
|
||||||
|
else socket.emit('ALERT', 'No such user found.');
|
||||||
|
}
|
||||||
|
else socket.emit('ALERT', 'Not authorized to do that.');
|
||||||
|
});
|
||||||
|
socket.on('UNBAN', (data: Object) => {
|
||||||
|
if(socket.nick === data['room']){
|
||||||
|
if(banlist.get(data['room']) && banlist.get(data['room'])[data['ip']]){
|
||||||
|
banlist.set(data['room'], Object.assign({}, banlist.get(data['room']), {[data['ip']]: null}));
|
||||||
|
socket.emit('ALERT', data['ip']+' was unbanned.');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
socket.emit('ALERT', 'That IP is not banned.');
|
||||||
|
}
|
||||||
|
else socket.emit('ALERT', 'Not authorized to do that.');
|
||||||
|
});
|
||||||
|
socket.on('LISTBAN', (data: Object) => {
|
||||||
|
if(socket.nick === data['room']){
|
||||||
|
if(banlist.get(data['room'])) {
|
||||||
|
let bans = Object.keys(banlist.get(data['room']));
|
||||||
|
let str = '';
|
||||||
|
for(let i=0;i<bans.length;i++){
|
||||||
|
str += bans[i]+', ';
|
||||||
|
}
|
||||||
|
socket.emit('ALERT', 'Banned IP adresses: '+str.substring(0, str.length - 2));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
socket.emit('ALERT', 'No one is banned from this room');
|
||||||
|
}
|
||||||
|
else socket.emit('ALERT', 'Not authorized to do that.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
//socketio spam
|
||||||
|
const socketAS = new socketSpam({
|
||||||
|
banTime: 20,
|
||||||
|
kickThreshold: 10,
|
||||||
|
kickTimesBeforeBan: 3,
|
||||||
|
banning: true,
|
||||||
|
io: io
|
||||||
|
});
|
||||||
|
socketAS.event.on('ban', (socket) => {
|
||||||
|
let rooms = Object.keys(socket.rooms);
|
||||||
|
for(let i=1;i<rooms.length;i++){
|
||||||
|
io.to(rooms[i]).emit('ALERT', socket.nick+' was banned.');
|
||||||
|
}
|
||||||
|
store.rm(socket.nick);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import * as mediaserver from "./server";
|
||||||
|
import * as db from "./database";
|
||||||
|
import * as http from "./http";
|
||||||
|
import * as cleanup from "./cleanup";
|
||||||
|
import { config } from "./config";
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
await db.init();
|
||||||
|
await cleanup.init();
|
||||||
|
await http.init();
|
||||||
|
await mediaserver.init();
|
||||||
|
console.log(`Satyr v${config['satyr']['version']} ready`);
|
||||||
|
}
|
||||||
|
run();
|
||||||
|
export { run };
|
212
src/irc.js
212
src/irc.js
|
@ -1,212 +0,0 @@
|
||||||
// written by crushv <nik@telekem.net>
|
|
||||||
// thanks nikki
|
|
||||||
|
|
||||||
const net = require('net')
|
|
||||||
const EventEmitter = require('events')
|
|
||||||
|
|
||||||
const socket = new net.Socket()
|
|
||||||
const emitter = new EventEmitter()
|
|
||||||
|
|
||||||
socket.setEncoding('utf8')
|
|
||||||
|
|
||||||
socket.on('error', console.error)
|
|
||||||
|
|
||||||
function m (text) {
|
|
||||||
console.log('> ' + text)
|
|
||||||
socket.write(text + '\r\n')
|
|
||||||
}
|
|
||||||
|
|
||||||
var config
|
|
||||||
|
|
||||||
socket.once('connect', async () => {
|
|
||||||
console.log('Connected')
|
|
||||||
m(`PASS ${config.pass} TS 6 :${config.sid}`)
|
|
||||||
m('CAPAB QS ENCAP EX IE SAVE EUID')
|
|
||||||
m(`SERVER ${config.server} 1 satyr`)
|
|
||||||
})
|
|
||||||
|
|
||||||
function parseLine (l) {
|
|
||||||
const colIndex = l.lastIndexOf(':')
|
|
||||||
if (colIndex > -1) {
|
|
||||||
return {
|
|
||||||
params: l.substring(0, colIndex - 1).split(' '),
|
|
||||||
query: l.substring(colIndex + 1)
|
|
||||||
}
|
|
||||||
} else return { params: l.split(' ') }
|
|
||||||
}
|
|
||||||
|
|
||||||
const servers = []
|
|
||||||
const users = {}
|
|
||||||
const channels = {}
|
|
||||||
|
|
||||||
const globalCommands = {
|
|
||||||
// PING :42X
|
|
||||||
// params: SID
|
|
||||||
PING: l => {
|
|
||||||
const { query } = parseLine(l)
|
|
||||||
m(`PONG :${query}`)
|
|
||||||
emitter.emit('ping')
|
|
||||||
},
|
|
||||||
// PASS hunter2 TS 6 :42X
|
|
||||||
// params: password, 'TS', TS version, SID
|
|
||||||
PASS: l => {
|
|
||||||
const { query } = parseLine(l)
|
|
||||||
// adds a server
|
|
||||||
servers.push(query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverCommands = {
|
|
||||||
// EUID nik 1 1569146316 +i ~nik localhost6.attlocal.net 0::1 42XAAAAAB * * :nik
|
|
||||||
// params: nickname, hopcount, nickTS, umodes, username, visible hostname, IP address, UID, real hostname, account name, gecos
|
|
||||||
EUID: l => {
|
|
||||||
const { params } = parseLine(l)
|
|
||||||
const user = {
|
|
||||||
nick: params[0],
|
|
||||||
nickTS: params[2],
|
|
||||||
modes: params[3],
|
|
||||||
username: params[4],
|
|
||||||
vhost: params[5],
|
|
||||||
ip: params[6],
|
|
||||||
uid: params[7]
|
|
||||||
}
|
|
||||||
users[user.uid] = user
|
|
||||||
},
|
|
||||||
// SJOIN 1569142987 #test +nt :42XAAAAAB
|
|
||||||
// params: channelTS, channel, simple modes, opt. mode parameters..., nicklist
|
|
||||||
SJOIN: l => {
|
|
||||||
const { params, query } = parseLine(l)
|
|
||||||
const channel = {
|
|
||||||
timestamp: params[0],
|
|
||||||
name: params[1],
|
|
||||||
modes: params.slice(2).join(' '),
|
|
||||||
nicklist: query.split(' ').map(uid => {
|
|
||||||
if (/[^0-9a-zA-Z]/.test(uid[0])) return { uid: uid.slice(1), mode: uid[0] }
|
|
||||||
else return { uid: uid, mode: '' }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
channels[channel.name] = channel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userCommands = {
|
|
||||||
// :42XAAAAAC PRIVMSG #test :asd
|
|
||||||
// params: target, msg
|
|
||||||
PRIVMSG: (l, source) => {
|
|
||||||
const { params, query } = parseLine(l)
|
|
||||||
emitter.emit('message', users[source].nick, params[0], query)
|
|
||||||
},
|
|
||||||
// :42XAAAAAC JOIN 1569149395 #test +
|
|
||||||
JOIN: (l, source) => {
|
|
||||||
const { params } = parseLine(l)
|
|
||||||
channels[params[1]].nicklist.push({
|
|
||||||
uid: source
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// :42XAAAAAC PART #test :WeeChat 2.6
|
|
||||||
PART: (l, source) => {
|
|
||||||
const { params } = parseLine(l)
|
|
||||||
for (let i = 0; i < channels[params[0]].nicklist.length; i++) {
|
|
||||||
if (channels[params[0]].nicklist[i].uid === source) {
|
|
||||||
channels[params[0]].nicklist.splice(i, 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
QUIT: (_l, source) => {
|
|
||||||
delete users[source]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function parser (l) {
|
|
||||||
const split = l.split(' ')
|
|
||||||
const cmd = split[0]
|
|
||||||
const args = split.slice(1).join(' ')
|
|
||||||
if (globalCommands[cmd]) return globalCommands[cmd](args)
|
|
||||||
if (cmd[0] === ':') {
|
|
||||||
const source = cmd.slice(1)
|
|
||||||
const subcmd = split[1]
|
|
||||||
const subargs = split.slice(2).join(' ')
|
|
||||||
if (servers.indexOf(source) > -1 && serverCommands[subcmd]) serverCommands[subcmd](subargs)
|
|
||||||
if (users[source] && userCommands[subcmd]) userCommands[subcmd](subargs, source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.on('data', data => {
|
|
||||||
data.split('\r\n')
|
|
||||||
.filter(l => l !== '')
|
|
||||||
.forEach(l => {
|
|
||||||
console.log('< ' + l)
|
|
||||||
parser(l)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
module.exports.connect = conf => new Promise((resolve, reject) => {
|
|
||||||
emitter.once('ping', resolve)
|
|
||||||
config = conf
|
|
||||||
socket.connect(config.port)
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
socket.write('QUIT\r\n')
|
|
||||||
process.exit()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
module.exports.events = emitter
|
|
||||||
|
|
||||||
const genTS = () => Math.trunc((new Date()).getTime() / 1000)
|
|
||||||
const genUID = () => {
|
|
||||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
|
||||||
var uid = ''
|
|
||||||
for (let i = 0; i < 6; i++) uid += chars.charAt(Math.floor(Math.random() * chars.length))
|
|
||||||
if (users[uid]) return genUID()
|
|
||||||
return config.sid + uid
|
|
||||||
}
|
|
||||||
const getUID = nick => {
|
|
||||||
for (const key in users) if (users[key].nick === nick) return key
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.registerUser = nick => {
|
|
||||||
const user = {
|
|
||||||
nick: nick,
|
|
||||||
nickTS: genTS(),
|
|
||||||
modes: '+i',
|
|
||||||
username: '~' + nick,
|
|
||||||
vhost: config.vhost,
|
|
||||||
ip: '0::1',
|
|
||||||
uid: genUID()
|
|
||||||
}
|
|
||||||
users[user.uid] = user
|
|
||||||
m(`EUID ${user.nick} 1 ${user.nickTS} ${user.modes} ~${user.nick} ${user.vhost} 0::1 ${user.uid} * * :${user.nick}`)
|
|
||||||
}
|
|
||||||
module.exports.unregisterUser = nick => {
|
|
||||||
const uid = getUID(nick)
|
|
||||||
m(`:${uid} QUIT :Quit: satyr`)
|
|
||||||
delete users[uid]
|
|
||||||
}
|
|
||||||
module.exports.join = (nick, channelName) => {
|
|
||||||
const uid = getUID(nick)
|
|
||||||
if (!channels[channelName]) {
|
|
||||||
const channel = {
|
|
||||||
timestamp: genTS(),
|
|
||||||
name: channelName,
|
|
||||||
modes: '+nt',
|
|
||||||
nicklist: [{ uid: uid, mode: '' }]
|
|
||||||
}
|
|
||||||
channels[channel.name] = channel
|
|
||||||
}
|
|
||||||
m(`:${uid} JOIN ${channels[channelName].timestamp} ${channelName} +`)
|
|
||||||
}
|
|
||||||
module.exports.part = (nick, channelName) => {
|
|
||||||
const uid = getUID(nick)
|
|
||||||
m(`:${uid} PART ${channelName} :satyr`)
|
|
||||||
for (let i = 0; i < channels[channelName].nicklist.length; i++) {
|
|
||||||
if (channels[channelName].nicklist[i].uid === uid) {
|
|
||||||
channels[channelName].nicklist.splice(i, 1)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
module.exports.send = (nick, channelName, message) => {
|
|
||||||
const uid = getUID(nick)
|
|
||||||
m(`:${uid} PRIVMSG ${channelName} :${message}`)
|
|
||||||
emitter.emit('message', nick, channelName, message)
|
|
||||||
}
|
|
|
@ -3,13 +3,14 @@ import * as dirty from "dirty";
|
||||||
import { mkdir, fstat, access } from "fs";
|
import { mkdir, fstat, access } from "fs";
|
||||||
import * as strf from "strftime";
|
import * as strf from "strftime";
|
||||||
import * as db from "./database";
|
import * as db from "./database";
|
||||||
|
import {config} from "./config";
|
||||||
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||||
const { exec, execFile } = require('child_process');
|
const { exec, execFile } = require('child_process');
|
||||||
|
|
||||||
const keystore = dirty();
|
const keystore = dirty();
|
||||||
|
|
||||||
function init (mediaconfig: any, satyrconfig: any) {
|
function init () {
|
||||||
const nms = new NodeMediaServer(mediaconfig);
|
const nms = new NodeMediaServer({logType: 0,rtmp: config['rtmp']});
|
||||||
nms.run();
|
nms.run();
|
||||||
|
|
||||||
nms.on('postPublish', (id, StreamPath, args) => {
|
nms.on('postPublish', (id, StreamPath, args) => {
|
||||||
|
@ -23,7 +24,7 @@ function init (mediaconfig: any, satyrconfig: any) {
|
||||||
session.reject();
|
session.reject();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if(app !== satyrconfig.privateEndpoint){
|
if(app !== config['media']['privateEndpoint']){
|
||||||
//app isn't at public endpoint if we've reached this point
|
//app isn't at public endpoint if we've reached this point
|
||||||
console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id);
|
console.log("[NodeMediaServer] Wrong endpoint, rejecting stream:",id);
|
||||||
session.reject();
|
session.reject();
|
||||||
|
@ -34,23 +35,23 @@ function init (mediaconfig: any, satyrconfig: any) {
|
||||||
//otherwise kill the session
|
//otherwise kill the session
|
||||||
db.query('select username,record_flag from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
|
db.query('select username,record_flag from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
|
||||||
if(results[0]){
|
if(results[0]){
|
||||||
//push to rtmp
|
//transcode to mpd after making sure directory exists
|
||||||
//execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-analyzeduration', '0', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', '-crf', '18', '-f', 'flv', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username], {maxBuffer: Infinity});
|
|
||||||
//push to mpd after making sure directory exists
|
|
||||||
keystore[results[0].username] = key;
|
keystore[results[0].username] = key;
|
||||||
mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, ()=>{;});
|
mkdir(config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username, { recursive : true }, ()=>{;});
|
||||||
while(true){
|
while(true){
|
||||||
if(session.audioCodec !== 0 && session.videoCodec !== 0){
|
if(session.audioCodec !== 0 && session.videoCodec !== 0){
|
||||||
execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.privateEndpoint+'/'+key, '-map', '0:2', '-map', '0:2', '-map', '0:2', '-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy', '-c:v:1', 'libx264', '-c:v:2', 'libx264', '-crf:1', '33', '-crf:2', '40', '-b:v:1', '3000K', '-b:v:2', '1500K', '-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/index.mpd'], {maxBuffer: Infinity});
|
transCommand(results[0].username, key).then((r) => {
|
||||||
|
execFile(config['media']['ffmpeg'], r, {maxBuffer: Infinity});
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
await sleep(300);
|
await sleep(300);
|
||||||
}
|
}
|
||||||
if(results[0].record_flag && satyrconfig.record){
|
if(results[0].record_flag && config['media']['record']){
|
||||||
console.log('[NodeMediaServer] Initiating recording for stream:',id);
|
console.log('[NodeMediaServer] Initiating recording for stream:',id);
|
||||||
mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => {
|
mkdir(config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username, { recursive : true }, (err) => {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
execFile(satyrconfig.ffmpeg, ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.prviateEndpoint+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], {
|
execFile(config['media']['ffmpeg'], ['-loglevel', 'fatal', '-i', 'rtmp://127.0.0.1:'+config['rtmp']['port']+'/'+config['media']['privateEndpoint']+'/'+key, '-vcodec', 'copy', '-acodec', 'copy', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+results[0].username+'/'+strf('%d%b%Y-%H%M')+'.mp4'], {
|
||||||
detached : true,
|
detached : true,
|
||||||
stdio : 'inherit',
|
stdio : 'inherit',
|
||||||
maxBuffer: Infinity
|
maxBuffer: Infinity
|
||||||
|
@ -74,7 +75,7 @@ function init (mediaconfig: any, satyrconfig: any) {
|
||||||
nms.on('donePublish', (id, StreamPath, args) => {
|
nms.on('donePublish', (id, StreamPath, args) => {
|
||||||
let app: string = StreamPath.split("/")[1];
|
let app: string = StreamPath.split("/")[1];
|
||||||
let key: string = StreamPath.split("/")[2];
|
let key: string = StreamPath.split("/")[2];
|
||||||
if(app === satyrconfig.privateEndpoint) {
|
if(app === config['media']['privateEndpoint']) {
|
||||||
db.query('update user_meta,users set user_meta.live=false where users.stream_key='+db.raw.escape(key));
|
db.query('update user_meta,users set user_meta.live=false where users.stream_key='+db.raw.escape(key));
|
||||||
db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
|
db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
|
||||||
if(results[0]) keystore.rm(results[0].username);
|
if(results[0]) keystore.rm(results[0].username);
|
||||||
|
@ -94,19 +95,48 @@ function init (mediaconfig: any, satyrconfig: any) {
|
||||||
}
|
}
|
||||||
//localhost can play from whatever endpoint
|
//localhost can play from whatever endpoint
|
||||||
//other clients must use private endpoint
|
//other clients must use private endpoint
|
||||||
if(app !== satyrconfig.publicEndpoint && !session.isLocal) {
|
if(app !== config['media']['publicEndpoint'] && !session.isLocal) {
|
||||||
console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id);
|
console.log("[NodeMediaServer] Non-local Play from private endpoint, rejecting client:",id);
|
||||||
session.reject();
|
session.reject();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//rewrite playpath to private endpoint serverside
|
//rewrite playpath to private endpoint serverside
|
||||||
//(hopefully)
|
//(hopefully)
|
||||||
if(app === satyrconfig.publicEndpoint) {
|
if(app === config['media']['publicEndpoint']) {
|
||||||
if(keystore[key]){
|
if(keystore[key]){
|
||||||
session.playStreamPath = '/'+satyrconfig.privateEndpoint+'/'+keystore[key];
|
session.playStreamPath = '/'+config['media']['privateEndpoint']+'/'+keystore[key];
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function transCommand(user: string, key: string): Promise<string[]>{
|
||||||
|
let args: string[] = ['-loglevel', 'fatal', '-y', '-i', 'rtmp://127.0.0.1:'+config['rtmp']['port']+'/'+config['media']['privateEndpoint']+'/'+key];
|
||||||
|
if(config['transcode']['adaptive']===true && config['transcode']['variants'] > 1) {
|
||||||
|
for(let i=0;i<config['transcode']['variants'];i++){
|
||||||
|
args = args.concat(['-map', '0:2']);
|
||||||
|
}
|
||||||
|
args = args.concat(['-map', '0:1', '-c:a', 'copy', '-c:v:0', 'copy']);
|
||||||
|
for(let i=1;i<config['transcode']['variants'];i++){
|
||||||
|
args = args.concat(['-c:v:'+i, 'libx264',]);
|
||||||
|
}
|
||||||
|
for(let i=1;i<config['transcode']['variants'];i++){
|
||||||
|
let crf: number = Math.floor(18 + (i * 8)) > 51 ? 51 : Math.floor(18 + (i * 7));
|
||||||
|
args = args.concat(['-crf:'+i, ''+crf]);
|
||||||
|
}
|
||||||
|
for(let i=1;i<config['transcode']['variants'];i++){
|
||||||
|
let bv: number = Math.floor((5000 / config['transcode']['variants']) * (config['transcode']['variants'] - i));
|
||||||
|
args = args.concat(['-b:v:'+i, ''+bv]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
args = args.concat(['-c:a', 'copy', '-c:v', 'copy']);
|
||||||
|
}
|
||||||
|
if(config['transcode']['format'] === 'dash')
|
||||||
|
args = args.concat(['-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30', '-f', 'dash', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+user+'/index.mpd']);
|
||||||
|
else if(config['transcode']['format'] === 'hls')
|
||||||
|
args = args.concat(['-remove_at_exit', '1', '-hls_time', '1', '-hls_list_size', '30', '-f', 'hls', config['http']['directory']+'/'+config['media']['publicEndpoint']+'/'+user+'/index.m3u8']);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
export { init };
|
export { init };
|
|
@ -4,6 +4,8 @@
|
||||||
<link rel="stylesheet" type="text/css" href="/local.css">
|
<link rel="stylesheet" type="text/css" href="/local.css">
|
||||||
<link rel="icon" type="image/svg" href="/logo.svg">
|
<link rel="icon" type="image/svg" href="/logo.svg">
|
||||||
<title>{{ sitename }}</title>
|
<title>{{ sitename }}</title>
|
||||||
|
{% block head %}
|
||||||
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
|
|
|
@ -36,6 +36,24 @@
|
||||||
else if(m.startsWith('/list')){
|
else if(m.startsWith('/list')){
|
||||||
socket.emit('LIST', {room: room});
|
socket.emit('LIST', {room: room});
|
||||||
}
|
}
|
||||||
|
else if(m.startsWith('/banlist')){
|
||||||
|
socket.emit('LISTBAN', {
|
||||||
|
room: room,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if(m.startsWith('/ban')){
|
||||||
|
socket.emit('BAN', {
|
||||||
|
room: room,
|
||||||
|
nick: m.split(' ')[1],
|
||||||
|
time: (1 * m.split(' ')[2])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if(m.startsWith('/unban')){
|
||||||
|
socket.emit('UNBAN', {
|
||||||
|
room: room,
|
||||||
|
ip: m.split(' ')[1]
|
||||||
|
});
|
||||||
|
}
|
||||||
else socket.emit('MSG', {room: room, msg: m});
|
else socket.emit('MSG', {room: room, msg: m});
|
||||||
document.getElementById('m').value = '';
|
document.getElementById('m').value = '';
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,9 +4,12 @@
|
||||||
<h4>Chatting</h4>
|
<h4>Chatting</h4>
|
||||||
The webclient for chat can be accessed on the streamer's page, or at <a href="https://{{ domain }}/chat">https://{{ domain }}/chat</a></br></br>
|
The webclient for chat can be accessed on the streamer's page, or at <a href="https://{{ domain }}/chat">https://{{ domain }}/chat</a></br></br>
|
||||||
The following commands are available:</br>
|
The following commands are available:</br>
|
||||||
`/nick kawen (password)` Password is only required if kawen is a registered user.</br>
|
<code><a>/nick kawen (password)</a></code> Password is only required if kawen is a registered user.</br>
|
||||||
`/join kawen` Join the chatroom for kawen's stream and leave the previous room.</br>
|
<code><a>/join kawen</a></code> Join the chatroom for kawen's stream and leave the previous room.</br>
|
||||||
`/kick cvcvcv` Available only in your own room if you are a streamer. Forcefully disconnect the user.</br>
|
<code><a>/kick cvcvcv</a></code> Available only in your own room if you are a streamer. Forcefully disconnect the user.</br>
|
||||||
|
<code><a>/ban cvcvcv (time)</a></code> Ban a user from your room. Bans are based on IP address. The optional time is in minutes. The default is 30.</br>
|
||||||
|
<code><a>/banlist</a></code> List the IPs currently banned from your room.</br>
|
||||||
|
<code><a>/unban (ip)</a></code> self explanatory</br>
|
||||||
|
|
||||||
<h4>Streaming</h4>
|
<h4>Streaming</h4>
|
||||||
Users should stream to <a>rtmp://{{ domain }}/stream/Stream-Key</a></br></br>
|
Users should stream to <a>rtmp://{{ domain }}/stream/Stream-Key</a></br></br>
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
{% extends "base.njk" %}
|
{% extends "base.njk" %}
|
||||||
|
{% block head %}
|
||||||
|
<script src="/videojs/video.min.js"></script>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css">
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<script>
|
<script>
|
||||||
function newPopup(url) {
|
function newPopup(url) {
|
||||||
|
@ -16,10 +20,11 @@ function newPopup(url) {
|
||||||
<iframe src="/chat?room={{ username }}" frameborder="0" style="width: 100%;height: 100%; min-height: 500px;" allowfullscreen></iframe>
|
<iframe src="/chat?room={{ username }}" frameborder="0" style="width: 100%;height: 100%; min-height: 500px;" allowfullscreen></iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>window.HELP_IMPROVE_VIDEOJS = false;</script>
|
</br>
|
||||||
<script src="/videojs/video.min.js"></script>
|
<noscript>The webclients for the stream and the chat require javascript, but feel free to use the direct links above!</br></br></noscript>
|
||||||
<link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css">
|
{{ about | escape }}
|
||||||
<script>
|
<script>
|
||||||
|
window.HELP_IMPROVE_VIDEOJS = false;
|
||||||
var player = videojs('live-video', {
|
var player = videojs('live-video', {
|
||||||
html: {
|
html: {
|
||||||
nativeCaptions: false,
|
nativeCaptions: false,
|
||||||
|
@ -34,7 +39,5 @@ function newPopup(url) {
|
||||||
type: 'application/dash+xml'
|
type: 'application/dash+xml'
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
</script></br>
|
</script>
|
||||||
<noscript>The webclients for the stream and the chat require javascript, but feel free to use the direct links above!</br></br></noscript>
|
|
||||||
{{ about | escape }}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
Reference in New Issue