Switch from transcode server in node-media-server to spawning ffmpeg processes

Change config to reflect that
ffmpeg processes cleanup after themselves even on SIGINT now, cleanup.ts only cleans the database now
Adaptive livestreaming!
This commit is contained in:
knotteye 2019-11-16 11:34:16 -06:00
parent f8b197502a
commit 7983b60f8d
5 changed files with 29 additions and 36 deletions

View File

@ -51,9 +51,3 @@ record = false
publicEndpoint = 'live' publicEndpoint = 'live'
privateEndpoint = 'stream' privateEndpoint = 'stream'
ffmpeg = '' ffmpeg = ''
[transcode]
hls = false
hlsFlags = '[hls_time=2:hls_list_size=3:hls_flags=delete_segments]'
dash = true
dashFlags = '[f=dash:window_size=3:extra_window_size=5]'

View File

@ -1,23 +1,10 @@
import * as db from "./database"; import * as db from "./database";
import * as read from "recursive-readdir";
import * as fs from "fs";
async function init(siteDir: string) { async function init() {
//If satyr is restarted in the middle of a stream //If satyr is restarted in the middle of a stream
//it causes problems //it causes problems
//Live flags in the database stay live //Live flags in the database stay live
await db.query('update user_meta set live=false'); await db.query('update user_meta set live=false');
//and stray m3u8 files will play the last
//few seconds of a stream back
try {
var files = await read(siteDir+'/live', ['!*.m3u8']);
}
catch (error) {}
if(files === undefined || files.length == 0) return;
for(let i=0;i<files.length;i++){
fs.unlinkSync(files[i]);
}
return;
} }
export { init }; export { init };

View File

@ -10,6 +10,7 @@ async function run() {
const bcryptcfg: object = config.bcrypt; const bcryptcfg: object = config.bcrypt;
const satyr: object = { const satyr: object = {
privateEndpoint: config.media.privateEndpoint, privateEndpoint: config.media.privateEndpoint,
publicEndpoint: config.media.publicEndpoint,
record: config.media.record, record: config.media.record,
registration: config.satyr.registration, registration: config.satyr.registration,
webFormat: config.satyr.webFormat, webFormat: config.satyr.webFormat,
@ -18,7 +19,8 @@ async function run() {
domain: config.satyr.domain, domain: config.satyr.domain,
email: config.satyr.email, email: config.satyr.email,
rootredirect: config.satyr.rootredirect, rootredirect: config.satyr.rootredirect,
version: process.env.npm_package_version version: process.env.npm_package_version,
directory: config.server.http.directory
}; };
const nms: object = { const nms: object = {
logType: config.server.logs, logType: config.server.logs,
@ -29,7 +31,7 @@ async function run() {
ping: config.server.rtmp.ping, ping: config.server.rtmp.ping,
ping_timeout: config.server.rtmp.ping_timeout, ping_timeout: config.server.rtmp.ping_timeout,
}, },
http: { /*http: {
port: config.server.http.port + 1, port: config.server.http.port + 1,
mediaroot: config.server.http.directory, mediaroot: config.server.http.directory,
allow_origin: config.server.http.allow_origin allow_origin: config.server.http.allow_origin
@ -45,7 +47,7 @@ async function run() {
dashFlags: config.transcode.dashFlags dashFlags: config.transcode.dashFlags
} }
] ]
}, },*/
auth: { auth: {
api: config.server.api, api: config.server.api,
api_user: config.server.api_user, api_user: config.server.api_user,
@ -54,7 +56,7 @@ async function run() {
}; };
db.init(dbcfg, bcryptcfg); db.init(dbcfg, bcryptcfg);
await cleanup.init(config.server.http.directory); await cleanup.init();
api.init(satyr); api.init(satyr);
http.init(satyr, config.server.http.port, config.ircd); http.init(satyr, config.server.http.port, config.ircd);
mediaserver.init(nms, satyr); mediaserver.init(nms, satyr);

View File

@ -1,7 +1,8 @@
import * as NodeMediaServer from "node-media-server"; import * as NodeMediaServer from "node-media-server";
import { mkdir } from "fs"; import { mkdir, fstat, access } from "fs";
import * as db from "./database"; import * as db from "./database";
const { exec } = require('child_process'); const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
const { exec, execFile } = require('child_process');
function init (mediaconfig: any, satyrconfig: any) { function init (mediaconfig: any, satyrconfig: any) {
const nms = new NodeMediaServer(mediaconfig); const nms = new NodeMediaServer(mediaconfig);
@ -18,7 +19,7 @@ function init (mediaconfig: any, satyrconfig: any) {
session.reject(); session.reject();
return false; return false;
} }
if(app === mediaconfig.trans.tasks[0].app) { if(app === satyrconfig.publicEndpoint) {
if(session.ip.includes('127.0.0.1') || session.ip === '::1') { if(session.ip.includes('127.0.0.1') || session.ip === '::1') {
//only allow publish to public endpoint from localhost //only allow publish to public endpoint from localhost
//this is NOT a comprehensive way of doing this, but I'm ignoring it //this is NOT a comprehensive way of doing this, but I'm ignoring it
@ -37,15 +38,15 @@ function init (mediaconfig: any, satyrconfig: any) {
return db.query('select username,record_flag from users where username=\''+key+'\' limit 1').then((results) => { return db.query('select username,record_flag from users where username=\''+key+'\' limit 1').then((results) => {
if(results[0].record_flag && satyrconfig.record){ if(results[0].record_flag && satyrconfig.record){
console.log('[NodeMediaServer] Initiating recording for stream:',id); console.log('[NodeMediaServer] Initiating recording for stream:',id);
mkdir(mediaconfig.http.mediaroot+'/'+mediaconfig.trans.tasks[0].app+'/'+results[0].username, { recursive : true }, (err) => { mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => {
if (err) throw err; if (err) throw err;
let subprocess = exec('ffmpeg -i rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+mediaconfig.trans.tasks[0].app+'/'+results[0].username+' -vcodec copy -acodec copy '+mediaconfig.http.mediaroot+'/'+mediaconfig.trans.tasks[0].app+'/'+results[0].username+'/$(date +%d%b%Y-%H%M).mp4',{ let subprocess = exec('ffmpeg -i rtmp://127.0.0.1:'+mediaconfig.rtmp.port+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+' -vcodec copy -acodec copy '+satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username+'/$(date +%d%b%Y-%H%M).mp4',{
detached : true, detached : true,
stdio : 'inherit' stdio : 'inherit'
}); });
subprocess.unref(); subprocess.unref();
//spawn an ffmpeg process to record the stream, then detach it completely //spawn an ffmpeg process to record the stream, then detach it completely
//ffmpeg can then finalize the recording if satyr crashes mid-stream //ffmpeg can then (probably) finalize the recording if satyr crashes mid-stream
}); });
} }
else { else {
@ -67,9 +68,18 @@ function init (mediaconfig: any, satyrconfig: any) {
session.reject(); session.reject();
return false; return false;
} }
db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then((results) => { db.query('select username from users where stream_key='+db.raw.escape(key)+' limit 1').then(async (results) => {
if(results[0]){ if(results[0]){
exec('ffmpeg -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+'/'+mediaconfig.trans.tasks[0].app+'/'+results[0].username); //push to rtmp
exec('ffmpeg -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);
//push to mpd after making sure directory exists
mkdir(satyrconfig.directory+'/'+satyrconfig.publicEndpoint+'/'+results[0].username, { recursive : true }, (err) => {;});
sleep(5000).then( () => {
//wait for stream to initialize, but i'm not happy about this
exec('ffmpeg -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');
});
//switch to execFile at some point, it's safer
//execFile('/usr/bin/ffmpeg',['-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]);
console.log('[NodeMediaServer] Stream key okay for stream:',id); console.log('[NodeMediaServer] Stream key okay for stream:',id);
} }
else{ else{
@ -81,7 +91,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 === mediaconfig.trans.tasks[0].app) { if(app === satyrconfig.publicEndpoint) {
db.query('update user_meta set live=false where username=\''+key+'\' limit 1'); db.query('update user_meta set live=false where username=\''+key+'\' limit 1');
} }
}); });

View File

@ -7,7 +7,7 @@ function newPopup(url) {
} }
</script> </script>
</br> </br>
<span style="float: left;font-size: large;"><a href="/live/{{ user }}/index.m3u8">{{ user }}</a> | {{ streamtitle | escape }}</b></span><span style="float: right;font-size: large;"> Links | <a href="rtmp://{{ domain }}/live/{{ user }}">Watch</a> <a href="JavaScript:newPopup('/chat?room={{ user }}');">Chat</a> <a href="/vods/{{ user }}">VODs</a></span> <span style="float: left;font-size: large;"><a href="/live/{{ user }}/index.mpd">{{ user }}</a> | {{ streamtitle | escape }}</b></span><span style="float: right;font-size: large;"> Links | <a href="rtmp://{{ domain }}/live/{{ user }}">Watch</a> <a href="JavaScript:newPopup('/chat?room={{ user }}');">Chat</a> <a href="/vods/{{ user }}">VODs</a></span>
<div id="jscontainer"> <div id="jscontainer">
<div id="jschild" style="width: 70%;height: 100%;"> <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%;"></video> <video controls poster="/thumbnail.jpg" class="video-js vjs-default-skin" id="live-video" style="width:100%;height:100%;"></video>