Compare commits

..

No commits in common. 'master' and 'v0.10.1' have entirely different histories.

  1. 11
      docs/CONFIGURATION.md
  2. 53
      docs/HWACCEL.md
  3. 5
      install/config.example.yml
  4. 21
      package-lock.json
  5. 8
      package.json
  6. 14
      site/dashjs/LICENSE.md
  7. 3
      site/dashjs/dash.all.min.js
  8. 2
      site/index.html
  9. 83
      site/index.js
  10. 385
      site/play.svg
  11. 22
      src/cleanup.ts
  12. 17
      src/config.ts
  13. 3
      src/database.ts
  14. 21
      src/db/3.ts
  15. 50
      src/http.ts
  16. 1
      src/migrate.ts
  17. 47
      src/server.ts
  18. 6
      src/v3manual.ts
  19. 110
      templates/user.njk

@ -38,16 +38,17 @@ transcode:
# satyr will generate one source quality variant, and the remaining
# variants will be of incrementally lower quality and bitrate
# having more than 4-5 variants will start giving diminishing returns on stream quality for cpu load
# if you can't afford to generate at least 3 variants, it's recommended to leave adaptive streaming off
inputflags: ""
# additional flags to apply to the input during transcoding
outputflags: ""
# additional flags to apply to the output during transcoding
hwaccel:
# See HWACCEL.md for information on configuring hardware acceleration.
# hardware acceleration is a bit difficult to configure programmatically
# this is a good place to do so for your system
# https://trac.ffmpeg.org/wiki/HWAccelIntro is a good place to start
# having more than 4-5 variants will start giving diminishing returns on stream quality for cpu load
# if you can't afford to generate at least 3 variants, it's recommended to leave adaptive streaming off
crypto:
saltRounds: 12

@ -1,53 +0,0 @@
## Configuration Hardware Acceleration
Satyr supports the NVENC and VA-API hardware acceleration APIs. If you've configured your system correctly (the hard part) it should be enough to set the type and use the default device setting if you only have one hardware acceleration device.
### System
Configuring the system for any hardware acceleration API involves three main steps: selecting the right drivers, installing the API libraries, and configuring ffmpeg.
#### NVENC
NVENC in ffmpeg can work with either open-source drivers (nouvea) or nvidia's proprietary drivers. The documentation for your distribution should have instructions for installing these.
The only system library you should need is the CUDA toolkit, general named cudatoolkit, nvidia-cuda-toolkit, or some variation in your system repositories.
You can also try installing manually from [here](https://developer.nvidia.com/cuda-downloads).
Most binary distributions provide a version of ffmpeg with NVENC already enabled. If not you can try compiling ffmpeg from source with the `--enable-nvenc` flag. If you use a source based distribution you should be familiar with enabling optional compile flags.
You can verify that ffmpeg has been set up correctly by checking the output of `ffmpeg -hide_banner -hwaccels | grep cuvid` and `ffmpeg -hide_banner -encoders | grep nvenc`. If you don't see anything, something is wrong.
#### VA-API
VA-API is an extremely generic API. Although the package names might be different in your distribution, the arch wiki page for hardware acceleration has good information on [driver selection](https://wiki.archlinux.org/index.php/Hardware_video_acceleration#Installation) and [verifying](https://wiki.archlinux.org/index.php/Hardware_video_acceleration#Verifying_VA-API) a VA-API install for a wide range of devices.
Regardless of driver selection, you will also need libva or the equivalent from your distrubtion, and libva-utils can be helpful as well.
Most binary distributions provide a version of ffmpeg with VA-API already enabled. If not you can try compiling ffmpeg from source with the `--enable-vaapi` flag. If you use a source based distribution you should be familiar with enabling optional compile flags.
You can verify that ffmpeg has been set up correctly by checking the output of `ffmpeg -hide_banner -hwaccels | grep vaapi` and `ffmpeg -hide_banner -encoders | grep vaapi`. If you don't see anything, something is wrong.
### Satyr
```
# Decoding
hwaccel:
# Enable hardware acceleration for decoding as well as encoding.
# Probably not worth it, hardware decoding won't be any faster compared to software on a vaguely modern CPU
# Hardware decoding also may not support the input format, in which case transcoding will fail
decode: true
# Only supported for VA-API
# Fall back to software decoding if hardware decoding fails
hwaccel:
decode: 'fallback'
# NVENC
hwaccel:
type: 'nvenc'
# device is optional for nvenc
device: 0
# nvenc wants a device number instead of a path, set to null to use the default
# VA-API
hwaccel:
type: 'vaapi'
# device is mandatory for va-api
device: '/dev/dri/renderD128'
```

@ -28,15 +28,10 @@ database:
transcode:
#may result in higher latency if your cpu can't keep up
adaptive: false
#more than 3 might cause problems when using hwacceleration
variants: 3
#unused right now, will always transcode to dash
format: dash
hwaccel:
# see docs/HWACCEL.md for instructions on configuring hardware acceleration
type: null
chat:
irc:

21
package-lock.json generated

@ -1,6 +1,6 @@
{
"name": "satyr",
"version": "0.10.2",
"version": "0.10.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -634,11 +634,6 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
},
"eme-encryption-scheme-polyfill": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.0.1.tgz",
"integrity": "sha512-Wz+Ro1c0/2Wsx2RLFvTOO0m4LvYn+7cSnq3XOvRvLLBq8jbvUACH/zpU9s0/5+mQa5oaelkU69x+q0z/iWYrFA=="
},
"encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@ -1110,9 +1105,9 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ini": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
"integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
},
"ipaddr.js": {
"version": "1.9.0",
@ -1952,14 +1947,6 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"shaka-player": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-3.0.5.tgz",
"integrity": "sha512-LYq56q9DA7yTLBD1yQwZrMlJZOovb2yRmo0C3AsddL1J0ee+U4BXr1QZd5amtpBvl8fOiLgkW/12UuChMR764A==",
"requires": {
"eme-encryption-scheme-polyfill": "^2.0.1"
}
},
"signal-exit": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",

@ -1,6 +1,6 @@
{
"name": "satyr",
"version": "1.0.0",
"version": "0.10.1",
"description": "A livestreaming server.",
"license": "AGPL-3.0",
"author": "knotteye",
@ -10,12 +10,11 @@
"setup": "sh install/setup.sh",
"migrate": "ts-node src/migrate.ts",
"invite": "ts-node src/cli.ts --invite",
"v3-manual": "ts-node src/v3manual.ts",
"make-templates": "nunjucks-precompile -i [\"\\.html$\",\"\\.njk$\"] templates > site/templates.js"
},
"repository": {
"type": "git",
"url": "https://git.waldn.net/git/knotteye/satyr.git"
"url": "https://gitlab.com/knotteye/satyr.git"
},
"dependencies": {
"bcrypt": "^5.0.0",
@ -39,8 +38,7 @@
"socket.io": "^2.3.0",
"strftime": "^0.10.0",
"ts-node": "^8.5.4",
"typescript": "^3.6.3",
"shaka-player": "^3.0.5"
"typescript": "^3.6.3"
},
"devDependencies": {
"@types/node": "^12.12.67"

@ -0,0 +1,14 @@
# dash.js BSD License Agreement
The copyright in this software is being made available under the BSD License, included below. This software may be subject to other third party and contributor rights, including patent rights, and no such rights are granted under this license.
**Copyright (c) 2015, Dash Industry Forum.
**All rights reserved.**
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
* Neither the name of the Dash Industry Forum nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
**THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.**

File diff suppressed because one or more lines are too long

@ -6,7 +6,7 @@
<script src="/nunjucks-slim.js"></script>
<script src="/templates.js"></script>
<script src="/shaka-player.compiled.js"></script>
<script src="/dashjs/dash.all.min.js"></script>
<script>
nunjucks.configure({ autoescape: true });

@ -82,7 +82,7 @@ async function render(path, s){
if(!config.title){document.body.innerHTML = nunjucks.render('404.njk', context); break;}
document.body.innerHTML = nunjucks.render('user.njk', Object.assign({about: config.about, title: config.title, username: config.username}, context));
modifyLinks();
initPlayer(usr);
startVideo();
break;
case (path.match(/^\/vods\/.+\/manage\/?$/) || {}).input: // /vods/:user/manage
var usr = path.substring(6, (path.length - 7));
@ -102,15 +102,12 @@ async function render(path, s){
//root
case "/":
render('/users/live');
modifyLinks();
break;
case "":
render('/users/live');
modifyLinks();
break;
case "/index.html":
render('/users/live');
modifyLinks();
break;
//404
default:
@ -170,7 +167,7 @@ function handleLoad() {
function modifyLinks() {
for (var ls = document.links, numLinks = ls.length, i=0; i<numLinks; i++){
if(ls[i].href.indexOf(location.protocol+'//'+location.host) !== -1 && ls[i].href.match(new RegExp(/\/\w+\.\w+$/)) === null) {
if(ls[i].href.indexOf(location.protocol+'//'+location.host) !== -1) {
//should be a regular link
ls[i].setAttribute('onclick', 'return internalLink(\"'+ls[i].href.substring((location.protocol+'//'+location.host).length)+'\")');
}
@ -182,53 +179,41 @@ function internalLink(path){
return false;
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
//start dash.js
async function startVideo(){
//var url = "/live/{{username}}/index.mpd";
//var player = dashjs.MediaPlayer().create();
//player.initialize(document.querySelector("#videoPlayer"), url, true);
//console.log('called startvideo');
while(true){
if(!document.querySelector('#videoPlayer'))
break;
var shakaPolyFilled = false;
async function initPlayer(usr) {
var manifestUri = document.location.protocol+'//'+document.location.host+'/live/'+usr+'/index.mpd';
if(!shakaPolyFilled){
shaka.polyfill.installAll();
shakaPolyFilled = true;
}
var live = JSON.parse(await makeRequest("GET", "/api/"+usr+"/config")).live;
if(live){
// Create a Player instance.
const video = document.getElementById('video');
const player = new shaka.Player(video);
// Listen for error events.
player.addEventListener('error', onErrorEvent);
if(window.location.pathname.substring(window.location.pathname.length - 1) !== '/'){
var url = "/api/"+window.location.pathname.substring(7)+"/config";
console.log(url)
var xhr = JSON.parse(await makeRequest("GET", url));
if(xhr.live){
var player = dashjs.MediaPlayer().create();
player.initialize(document.querySelector("#videoPlayer"), url, true);
break;
}
}
video.addEventListener('play', () => {
document.getElementById('playbtn').style.visibility = 'hidden';
});
video.addEventListener('pause', () => {
document.getElementById('playbtn').style.visibility = 'visible';
});
// Try to load a manifest.
// This is an asynchronous process.
try {
await player.load(manifestUri);
// This runs if the asynchronous load is successful.
console.log('The video has now been loaded!');
} catch (e) {
// onError is executed if the asynchronous load fails.
onError(e);
else{
var url = "/api/"+window.location.pathname.substring(7, window.location.pathname.length - 1)+"/config";
console.log(url)
var xhr = JSON.parse(await makeRequest("GET", url));
if(xhr.live){
var player = dashjs.MediaPlayer().create();
player.initialize(document.querySelector("#videoPlayer"), url, true);
break;
}
}
await sleep(60000);
}
} else {
if(document.getElementById('video') !== null)
setTimeout(initPlayer, 5000, usr);
}
}
function onErrorEvent(event) {
// Extract the shaka.util.Error object from the event.
onError(event.detail);
}
function onError(error) {
// Log the error.
console.error('Error code', error.code, 'object', error);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

@ -1,385 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:ns1="http://sozi.baierouge.fr"
id="svg1307"
sodipodi:docname="media-playback-start.svg"
inkscape:export-filename="/home/lapo/Desktop/Grafica/Icone/media-actions-outlines.png"
viewBox="0 0 48 48"
sodipodi:version="0.32"
inkscape:export-xdpi="90"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
inkscape:export-ydpi="90"
inkscape:version="0.46"
sodipodi:docbase="/home/jimmac/src/cvs/tango-icon-theme/scalable/actions"
>
<defs
id="defs1309"
>
<linearGradient
id="linearGradient2306"
y2="95"
gradientUnits="userSpaceOnUse"
x2="70.827"
gradientTransform="translate(-45 -71.094)"
y1="124.12"
x1="71.289"
inkscape:collect="always"
>
<stop
id="stop5077"
style="stop-color:#adb0a8"
offset="0"
/>
<stop
id="stop5079"
style="stop-color:#464744"
offset="1"
/>
</linearGradient
>
<radialGradient
id="radialGradient2314"
gradientUnits="userSpaceOnUse"
cy="83.991"
cx="107.59"
gradientTransform="matrix(.053243 -.83624 2.0195 .12857 -151.92 108.08)"
r="12.552"
inkscape:collect="always"
>
<stop
id="stop2693"
style="stop-color:#ffffff"
offset="0"
/>
<stop
id="stop2695"
style="stop-color:#d3d7cf"
offset="1"
/>
</radialGradient
>
<linearGradient
id="linearGradient2690"
y2="88.924"
gradientUnits="userSpaceOnUse"
x2="70.952"
gradientTransform="matrix(1.1282 0 0 1.1282 -53.993 -83.36)"
y1="101.74"
x1="70.914"
inkscape:collect="always"
>
<stop
id="stop2686"
style="stop-color:#ffffff"
offset="0"
/>
<stop
id="stop2688"
style="stop-color:#000000"
offset="1"
/>
</linearGradient
>
</defs
>
<sodipodi:namedview
id="base"
inkscape:showpageshadow="false"
inkscape:zoom="1"
borderopacity="0.19607843"
inkscape:current-layer="layer1"
stroke="#555753"
guidetolerance="1px"
fill="#555753"
inkscape:grid-points="true"
inkscape:grid-bbox="true"
showgrid="false"
showguides="false"
bordercolor="#666666"
inkscape:window-x="326"
inkscape:guide-bbox="true"
inkscape:window-y="160"
inkscape:window-width="872"
inkscape:pageopacity="0.0000000"
inkscape:pageshadow="2"
pagecolor="#ffffff"
inkscape:cx="-117.42449"
inkscape:cy="12.980288"
inkscape:document-units="px"
inkscape:window-height="688"
showborder="true"
>
<sodipodi:guide
id="guide2194"
position="38.996647"
orientation="horizontal"
/>
<sodipodi:guide
id="guide2196"
position="9.0140845"
orientation="horizontal"
/>
<sodipodi:guide
id="guide2198"
position="9.0140845"
orientation="vertical"
/>
<sodipodi:guide
id="guide2200"
position="38.975184"
orientation="vertical"
/>
<sodipodi:guide
id="guide2202"
position="22.988281"
orientation="horizontal"
/>
<sodipodi:guide
id="guide2204"
position="23.908786"
orientation="vertical"
/>
<sodipodi:guide
id="guide4332"
position="157.99417"
orientation="vertical"
/>
<sodipodi:guide
id="guide4334"
position="-36.062446"
orientation="horizontal"
/>
<sodipodi:guide
id="guide4336"
position="-58.02695"
orientation="horizontal"
/>
<sodipodi:guide
id="guide4338"
position="180.00287"
orientation="vertical"
/>
<sodipodi:guide
id="guide4417"
position="107.92217"
orientation="vertical"
/>
<sodipodi:guide
id="guide4419"
position="129.93087"
orientation="vertical"
/>
<sodipodi:guide
id="guide5106"
position="19.996875"
orientation="horizontal"
/>
<sodipodi:guide
id="guide5119"
position="63.039674"
orientation="horizontal"
/>
<sodipodi:guide
id="guide5121"
position="49.066305"
orientation="horizontal"
/>
<sodipodi:guide
id="guide5307"
position="-86.007168"
orientation="horizontal"
/>
<sodipodi:guide
id="guide5309"
position="-108.09009"
orientation="horizontal"
/>
<sodipodi:guide
id="guide3111"
position="-100.15429"
orientation="horizontal"
/>
<inkscape:grid
id="GridFromPre046Settings"
opacity=".2"
color="#0000ff"
originy="0px"
originx="0px"
empspacing="2"
spacingy="0.5px"
spacingx="0.5px"
empopacity="0.4"
type="xygrid"
empcolor="#0000ff"
/>
</sodipodi:namedview
>
<g
id="layer1"
inkscape:label="Layer 1"
inkscape:groupmode="layer"
>
<path
id="path2682"
style="stroke-linejoin:round;opacity:.15;color:#000000;stroke:url(#linearGradient2690);stroke-linecap:square;stroke-width:2;fill:none"
inkscape:r_cy="true"
inkscape:r_cx="true"
sodipodi:nodetypes="cccc"
d="m12 39.5v-30.5l26.07 14.817-26.07 15.683z"
/>
<path
id="path3375"
style="fill-rule:evenodd;color:#000000;fill:url(#radialGradient2314)"
inkscape:r_cy="true"
inkscape:r_cx="true"
sodipodi:nodetypes="cccc"
d="m12.499 37.811v-27.811l24.104 13.906-24.104 13.905z"
/>
<path
id="path2479"
style="stroke-linejoin:round;color:#000000;stroke:url(#linearGradient2306);stroke-linecap:square;fill:none"
inkscape:r_cy="true"
inkscape:r_cx="true"
sodipodi:nodetypes="cccc"
d="m12.499 37.811v-27.811l24.104 13.906-24.104 13.905z"
/>
<path
id="path2481"
style="fill-rule:evenodd;color:#000000;fill:#ffffff"
inkscape:r_cy="true"
inkscape:r_cx="true"
sodipodi:nodetypes="cccccccc"
d="m12.999 10.874v26.063l22.594-13.031-22.594-13.032zm1 1.75l19.563 11.282-19.563 11.281v-22.563z"
/>
<path
id="path2339"
style="opacity:.5;color:#000000;display:block;fill:#ffffff"
inkscape:r_cy="true"
inkscape:r_cx="true"
sodipodi:nodetypes="cccc"
d="m13.938 12.562v11.688c4.269-0.045 9.164-0.346 17.062-1.875l-17.062-9.813z"
/>
</g
>
<metadata
>
<rdf:RDF
>
<cc:Work
>
<dc:format
>image/svg+xml</dc:format
>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"
/>
<cc:license
rdf:resource="http://creativecommons.org/licenses/publicdomain/"
/>
<dc:publisher
>
<cc:Agent
rdf:about="http://openclipart.org/"
>
<dc:title
>Openclipart</dc:title
>
</cc:Agent
>
</dc:publisher
>
<dc:title
>tango media start</dc:title
>
<dc:date
>2010-03-11T09:12:20</dc:date
>
<dc:description
>"Play" or "start" icon from &lt;A href="http://tango.freedesktop.org/Tango_Desktop_Project"&gt; Tango Project &lt;/A&gt; &#13;\n&lt;BR&gt;&lt;BR&gt;&#13;\nSince version 0.8.90 Tango Project icons are Public Domain: &lt;A href="http://tango.freedesktop.org/Frequently_Asked_Questions#Terms_of_Use.3F"&gt; Tango Project FAQ &lt;/A&gt;</dc:description
>
<dc:source
>https://openclipart.org/detail/31003/tango-media-start-by-warszawianka</dc:source
>
<dc:creator
>
<cc:Agent
>
<dc:title
>warszawianka</dc:title
>
</cc:Agent
>
</dc:creator
>
<dc:subject
>
<rdf:Bag
>
<rdf:li
>audio</rdf:li
>
<rdf:li
>button</rdf:li
>
<rdf:li
>externalsource</rdf:li
>
<rdf:li
>icon</rdf:li
>
<rdf:li
>play</rdf:li
>
<rdf:li
>playback</rdf:li
>
<rdf:li
>sign</rdf:li
>
<rdf:li
>start</rdf:li
>
<rdf:li
>symbol</rdf:li
>
<rdf:li
>tango</rdf:li
>
<rdf:li
>triangle</rdf:li
>
</rdf:Bag
>
</dc:subject
>
</cc:Work
>
<cc:License
rdf:about="http://creativecommons.org/licenses/publicdomain/"
>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Reproduction"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#Distribution"
/>
<cc:permits
rdf:resource="http://creativecommons.org/ns#DerivativeWorks"
/>
</cc:License
>
</rdf:RDF
>
</metadata
>
</svg
>

Before

Width:  |  Height:  |  Size: 9.8 KiB

@ -9,8 +9,11 @@ async function init() {
if(tmp.length === 0){
console.log('No database version info, running initial migration.');
await require('./db/0').run();
await bringUpToDate();
}
else {
await bringUpToDate();
}
await bringUpToDate();
}
else {
console.log('Skipping database version check.');
@ -31,19 +34,16 @@ async function init() {
}
async function bringUpToDate(): Promise<void>{
var versions: Object[] = JSON.parse(JSON.stringify(await db.query('select * from db_meta'))); //ugh, don't ask
var scripts: any[] = readdirSync('./src/db/', {withFileTypes: false});
if(scripts.length - versions.length === 0){
var versions: Object[] = await db.query('select * from db_meta');
var scripts: Buffer[] | string[] = readdirSync('./src/db/', {withFileTypes: false});
var diff: number = scripts.length - versions.length
if(diff === 0){
console.log('No migration needed.');
} else {
console.log('Versions differ, migrating now.');
var diff: string[] = scripts.filter(n => {
//we have to use versions.some because {version: 0} === {version: 0} returns false lmao
return !versions.some(o => o['version']+''=== n.substring(0, n.length - 3))
});
for(let i=0;i<diff.length;i++){
console.log('Running migration '+diff[i]);
await require('./db/'+diff[i]).run();
for(let i=0;i<diff;i++){
console.log('Migration to version '+Math.floor(scripts.length-(diff-i)));
await require('./db/'+scripts[Math.floor(scripts.length-(diff-i))]).run();
}
console.log('Done migrating database.');
}

@ -4,17 +4,7 @@ try {
var localconfig: Object = parse(read('config/config.yml'));
console.log('Config file found.');
} catch (e) {
if(e['reason']) {
console.log('Error parsing config on line '+e['mark']['line']+', with reason: '+e['reason']);
}
else {
console.log('Config Error: '+e['code']);
switch(e['code']){
case 'ENOENT':
console.log('Does the file exist?');
break;
}
}
console.log('No config file found. Exiting.');
process.exit();
}
const config: Object = {
@ -39,11 +29,6 @@ const config: Object = {
connectionTimeout: '1000',
insecureAuth: false,
debug: false }, localconfig['database']),
hwaccel: Object.assign({
type: null,
device: null,
decode: false
}, localconfig['hwaccel']),
rtmp: Object.assign({
cluster: false,
port: 1935,

@ -27,7 +27,7 @@ async function addUser(name: string, password: string){
let dupe = await query('select * from users where username='+raw.escape(name));
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 user_meta (username, title, about, live) VALUES ('+raw.escape(name)+',\' \',\' \',false)');
await query('INSERT INTO user_meta (username, title, about, live) VALUES ('+raw.escape(name)+',\'\',\'\',false)');
await query('INSERT INTO chat_integration (username, irc, xmpp, twitch, discord) VALUES ('+raw.escape(name)+',\'\',\'\',\'\',\'\')');
await query('INSERT INTO twitch_mirror (username) VALUES ('+raw.escape(name)+')');
return true;
@ -40,7 +40,6 @@ async function rmUser(name: string){
await query('delete from user_meta where username='+raw.escape(name)+' limit 1');
await query('delete from chat_integration where username='+raw.escape(name)+' limit 1');
await query('delete from twitch_mirror where username='+raw.escape(name)+' limit 1');
await query('delete from ch_bans where channel='+raw.escape(name));
return true;
}

@ -1,21 +0,0 @@
import * as db from "../database";
import * as dirty from "dirty";
async function run () {
await db.query('CREATE TABLE IF NOT EXISTS ch_bans(channel VARCHAR(25), target VARCHAR(45), time BIGINT, length INT DEFAULT 30)');
console.log('!!! This migration has a race condition when run from the `npm run migrate` command. If thats how this was called, please re-run this migration manually.\n!!! Run `npm run v3-manual` to do so');
var bansdb = new dirty('./config/bans.db')
bansdb.on('load', async () => {
bansdb.forEach(async (key, val) => {
let ips = Object.keys(val);
for(var i=0;i<ips.length;i++){
await db.query('INSERT INTO ch_bans (channel, target, time, length) VALUES ('+db.raw.escape(key)+', '+db.raw.escape(ips[i])+', '+val[ips[i]].time+', '+val[ips[i]].length+')');
}
});
await db.query('INSERT INTO db_meta (version) VALUES (3)');
console.log('Done migrating bans.db');
console.log('If this was a manual migration, you can kill the process now.');
});
}
export { run }

@ -17,6 +17,7 @@ const app = express();
const server = http.createServer(app);
const io = socketio(server);
const store = dirty();
var banlist;
var jwkey;
try{
jwkey = JWK.asKey(readFileSync('./config/jwt.pem'));
@ -58,13 +59,6 @@ async function init(){
await initAPI();
//static files if nothing else matches
app.use(express.static(config['http']['directory']));
//Fake static files
app.get('/shaka-player.compiled.js', (req, res) => {
res.sendFile(process.cwd()+'/node_modules/shaka-player/dist/shaka-player.compiled.js');
});
app.get('/shaka-player.compiled.map', (req, res) => {
res.sendFile(process.cwd()+'/node_modules/shaka-player/dist/shaka-player.compiled.map');
});
//client-side site routes
if(!config['http']['server_side_render'])
await initFE();
@ -76,7 +70,7 @@ async function init(){
else res.status(404).render('404.njk', njkconf);
//res.status(404).render('404.njk', njkconf);
});
await initChat();
banlist = new dirty('./config/bans.db').on('load', () => {initChat()});
server.listen(config['http']['port']);
}
@ -253,7 +247,6 @@ async function initAPI() {
api.register(req.body.username, req.body.password, req.body.confirm, true).then((result) => {
if(result[0]) return genToken(req.body.username).then((t) => {
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
res.cookie('X-Auth-As', req.body.username, {maxAge: 604800000, httpOnly: false, sameSite: 'Lax'})
res.json(result);
api.useInvite(req.body.invite);
return;
@ -268,7 +261,6 @@ async function initAPI() {
api.register(req.body.username, req.body.password, req.body.confirm).then( (result) => {
if(result[0]) return genToken(req.body.username).then((t) => {
res.cookie('Authorization', t, {maxAge: 604800000, httpOnly: true, sameSite: 'Lax'});
res.cookie('X-Auth-As', req.body.username, {maxAge: 604800000, httpOnly: false, sameSite: 'Lax'})
res.json(result);
return;
});
@ -590,10 +582,9 @@ async function initChat() {
socket.on('JOINROOM', async (data) => {
let t: any = await db.query('select username from users where username='+db.raw.escape(data));
if(t[0]){
let b = await db.query('select * from ch_bans where target='+db.raw.escape(socket['handshake']['address'])+' and channel='+db.raw.escape(data));
if(b[0]){
if(Math.floor(b[0].time + (b[0].length * 60)) < Math.floor(Date.now() / 1000)){
await db.query('delete from ch_bans where target='+db.raw.escape(b[0].target)+'and channel='+db.raw.escape(b[0].channel)+' and time='+db.raw.escape(b[0].time)+' and length='+db.raw.escape(b[0].length));
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');
@ -682,27 +673,23 @@ async function initChat() {
}
else socket.emit('ALERT', 'Not authorized to do that.');
});
socket.on('BAN', async (data: Object) => {
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))
await db.query('insert into ch_bans (channel, target, time, length) VALUES ('+db.raw.escape(data['room'])+', '+db.raw.escape(target.ip)+', '+db.raw.escape(Math.floor(Date.now() / 1000))+', '+db.raw.escape(data['time'])+')');
else
await db.query('insert into ch_bans (channel, target, time, length) VALUES ('+db.raw.escape(data['room'])+', '+db.raw.escape(target.ip)+', '+db.raw.escape(Math.floor(Date.now() / 1000))+', '+db.raw.escape(30)+')');
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))
await db.query('insert into ch_bans (channel, target, time, length) VALUES ('+db.raw.escape(data['room'])+', '+db.raw.escape(target.ip)+', '+db.raw.escape(Math.floor(Date.now() / 1000))+', '+db.raw.escape(data['time'])+')');
else
await db.query('insert into ch_bans (channel, target, time, length) VALUES ('+db.raw.escape(data['room'])+', '+db.raw.escape(target.ip)+', '+db.raw.escape(Math.floor(Date.now() / 1000))+', '+db.raw.escape(30)+')');
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.');
}
@ -710,11 +697,10 @@ async function initChat() {
}
else socket.emit('ALERT', 'Not authorized to do that.');
});
socket.on('UNBAN', async (data: Object) => {
socket.on('UNBAN', (data: Object) => {
if(socket.nick === data['room']){
let b = await db.query('select * from ch_bans where channel='+db.raw.escape(data['room'])+' and target='+db.raw.escape(data['ip']));
if(b[0]){
await db.query('delete from ch_bans where channel='+db.raw.escape(data['room'])+' and target='+db.raw.escape(data['ip']));
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
@ -722,13 +708,13 @@ async function initChat() {
}
else socket.emit('ALERT', 'Not authorized to do that.');
});
socket.on('LISTBAN', async (data: Object) => {
socket.on('LISTBAN', (data: Object) => {
if(socket.nick === data['room']){
let b = await db.query('select target from ch_bans where channel='+db.raw.escape(data['room']));
if(b[0]) {
if(banlist.get(data['room'])) {
let bans = Object.keys(banlist.get(data['room']));
let str = '';
for(let i=0;i<b.length;i++){
str += b[i].target+', ';
for(let i=0;i<bans.length;i++){
str += bans[i]+', ';
}
socket.emit('ALERT', 'Banned IP adresses: '+str.substring(0, str.length - 2));
return;

@ -3,7 +3,6 @@ import {init as clean} from "./cleanup";
import { config } from "./config";
async function run() {
process.argv = process.argv.concat(['--skip-compile']);
await initDB();
await clean();
}

@ -42,10 +42,9 @@ function init () {
if(session.audioCodec !== 0 && session.videoCodec !== 0){
transCommand(results[0].username, key).then((r) => {
execFile(config['media']['ffmpeg'], r, {maxBuffer: Infinity}, (err, stdout, stderr) => {
//console.log(r);
//console.log(err);
//console.log(stdout);
//console.log(stderr);
/*console.log(err);
console.log(stdout);
console.log(stderr);*/
});
});
break;
@ -127,51 +126,29 @@ function init () {
async function transCommand(user: string, key: string): Promise<string[]>{
let args: string[] = ['-loglevel', 'fatal', '-y'];
let vcodec: string = 'libx264';
if(config['hwaccel']['type'] === 'nvenc'){
vcodec = 'h264_nvenc';
if(config['hwaccel']['decode']){
args = args.concat(['-hwaccel', 'cuda']);
if(config['hwaccel']['device'])
args = args.concat(['-hwaccel_device', config['hwaccel']['device']]);
args = args.concat(['-hwaccel_output_format', 'cuda']);
}
}
else if (config['hwaccel']['type'] === 'vaapi') {
vcodec = 'h264_vaapi';
if(config['hwaccel']['decode'] === 'fallback'){
args = args.concat('init_hw_device', 'vaapi=foo:'+config['hwaccel']['device'], '-hwaccel vaapi', '-hwaccel_output_format', 'vaapi', '-hwaccel_device', 'foo');
} else if (config['hwaccel']['decode']) {
args = args.concat(['-hwaccel', 'vaapi', '-hwaccel_output_format', 'vaapi', '-vaapi_device', config['hwaccel']['device']]);
}
}
if(config['transcode']['inputflags'] !== null && config['transcode']['inputflags'] !== "") args = args.concat(config['transcode']['inputflags'].split(" "));
args = args.concat(['-i', 'rtmp://127.0.0.1:'+config['rtmp']['port']+'/'+config['media']['privateEndpoint']+'/'+key, '-movflags', '+faststart']);
if(config['transcode']['adaptive']===true && config['transcode']['variants'] > 1) {
for(let i=0;i<config['transcode']['variants'];i++){
args = args.concat(['-map', '0:v']);
args = args.concat(['-map', '0:2']);
}
args = args.concat(['-map', '0:a', '-c:a', 'aac', '-c:v:0', vcodec]);
args = args.concat(['-map', '0:1', '-c:a', 'aac', '-c:v:0', 'libx264']);
for(let i=1;i<config['transcode']['variants'];i++){
args = args.concat(['-c:v:'+i, vcodec,]);
args = args.concat(['-c:v:'+i, 'libx264',]);
}
if(!config['hwaccel']['type']){
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 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((10000 / config['transcode']['variants']) * (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', 'aac', '-c:v', vcodec]);
args = args.concat(['-c:a', 'aac', '-c:v', 'libx264']);
}
if(!config['hwaccel']['type'])
args = args.concat(['-preset', 'veryfast']);
args = args.concat(['-tune', 'zerolatency']);
args = args.concat(['-preset', 'veryfast', '-tune', 'zerolatency']);
//if(config['transcode']['format'] === 'dash')
args = args.concat(['-remove_at_exit', '1', '-seg_duration', '1', '-window_size', '30']);
if(config['transcode']['outputflags'] !== null && config['transcode']['outputflags'] !== "") args = args.concat(config['transcode']['outputflags'].split(" "));

@ -1,6 +0,0 @@
import * as db from "./database";
async function main() {
await db.init();
await require('./db/3.ts').run();
}
main();

@ -1,56 +1,47 @@
{% extends "base.njk" %}
{% block head %}
<script src="/shaka-player.compiled.js"></script>
<script>
shakaPolyFilled = false;
var manifestUri = document.location.protocol+'//'+document.location.host+'/live/{{ username }}/index.mpd';
async function initPlayer() {
console.log('Trying to initialize player.');
if(!shakaPolyFilled){
shaka.polyfill.installAll();
shakaPolyFilled = true;
}
var live = JSON.parse(await makeRequest("GET", "/api/{{ username }}/config")).live;
if(live){
// Create a Player instance.
const video = document.getElementById('video');
const player = new shaka.Player(video);
// Listen for error events.
player.addEventListener('error', onErrorEvent);
<!--<script src="/videojs/video.min.js"></script>
<link rel="stylesheet" type="text/css" href="/videojs/video-js.min.css">-->
<script src="/dashjs/dash.all.min.js"></script>
<script>
async function startVideo(){
//var url = "/live/{{username}}/index.mpd";
//var player = dashjs.MediaPlayer().create();
//player.initialize(document.querySelector("#videoPlayer"), url, true);
//console.log('called startvideo');
while(true){
if(!document.querySelector('#videoPlayer'))
break;
video.addEventListener('play', () => {
document.getElementById('playbtn').style.visibility = 'hidden';
});
video.addEventListener('pause', () => {
document.getElementById('playbtn').style.visibility = 'visible';
});
// Try to load a manifest.
// This is an asynchronous process.
try {
await player.load(manifestUri);
// This runs if the asynchronous load is successful.
console.log('The video has now been loaded!');
} catch (e) {
// onError is executed if the asynchronous load fails.
onError(e);
}
} else {
setTimeout(initPlayer, 5000);
}
}
if(window.location.pathname.substring(window.location.pathname.length - 1) !== '/'){
var url = "/api/"+window.location.pathname.substring(7)+"/config";
console.log(url)
var xhr = JSON.parse(await makeRequest("GET", url));
if(xhr.live){
var player = dashjs.MediaPlayer().create();
player.initialize(document.querySelector("#videoPlayer"), url, true);
break;
}
}
function onErrorEvent(event) {
// Extract the shaka.util.Error object from the event.
onError(event.detail);
else{
var url = "/api/"+window.location.pathname.substring(7, window.location.pathname.length - 1)+"/config";
console.log(url)
var xhr = JSON.parse(await makeRequest("GET", url));
if(xhr.live){
var player = dashjs.MediaPlayer().create();
player.initialize(document.querySelector("#videoPlayer"), url, true);
break;
}
}
await sleep(60000);
}
}
function onError(error) {
// Log the error.
console.error('Error code', error.code, 'object', error);
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
document.addEventListener('DOMContentLoaded', initPlayer);
</script>
</script>
{% endblock %}
{% block content %}
<script>
@ -62,9 +53,13 @@ function newPopup(url) {
</br>
<span style="float: left;font-size: large;"><a href="/live/{{ username }}/index.mpd">{{ username }}</a> | {{ title | escape }}</b></span><span style="float: right;font-size: large;"> Links | <a href="rtmp://{{ domain }}/live/{{ username }}">Watch</a> <a href="JavaScript:newPopup('/chat?room={{ username }}');">Chat</a> <a href="/vods/{{ username }}">VODs</a></span>
<div id="jscontainer">
<div id="jschild" style="width: 70%;height: 100%;position: relative;">
<image id="playbtn" src="/play.svg" alt="Play Stream" style="width:100%;height:100%;width: 950px;height: 534px;position: absolute;" onclick="document.getElementById('video').paused ? document.getElementById('video').play() : document.getElementById('video') .pause();"></image>
<video id="video" style="width:100%;height:100%;width: 950px;height: 534px;background-color: rgb(0, 0, 0);" poster="/thumbnail.jpg" autoplay onclick="this.paused ? this.play() : this.pause();"></video>
<div id="jschild" style="width: 70%;height: 100%;">
<!--<video controls poster="/thumbnail.jpg" class="video-js vjs-default-skin" id="live-video" style="width:100%;height:100%;min-height: 500px;"></video>-->
<video id="videoPlayer" style="width:100%;height:100%;width: 950px;height: 534px;background-color: rgb(0, 0, 0);" poster="/thumbnail.jpg" autoplay muted></video>
<!--this spits errors fucking constantly after it tries to reload a video that's already running.. I dunno if it's bad or causing problems so let's just push it to develop and wait for issues!-->
<!--it plays the stream without reloading the page tho lol-->
<script>startVideo()</script>
</div>
<div id="jschild" class="webchat" style="width: 30%;height: 100%;position: relative;">
<iframe src="/chat?room={{ username }}" frameborder="0" style="width: 100%;height: 100%; min-height: 534px;" allowfullscreen></iframe>
@ -73,4 +68,21 @@ function newPopup(url) {
</br>
<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 }}
<!--<script>
window.HELP_IMPROVE_VIDEOJS = false;
var player = videojs('live-video', {
html: {
nativeCaptions: false,
},
});
player.ready(function() {
player.on("error", () => {
document.querySelector(".vjs-modal-dialog-content").textContent = "The stream is currently offline.";
});
player.src({
src: '/live/{{ username }}/index.mpd',
type: 'application/dash+xml'
});
})
</script>-->
{% endblock %}