diff --git a/lib/saito/core/server.ts b/lib/saito/core/server.ts index 77231ca334..758dfac945 100644 --- a/lib/saito/core/server.ts +++ b/lib/saito/core/server.ts @@ -325,19 +325,17 @@ class Server { // eslint-disable-next-line @typescript-eslint/no-var-requires const ws = require('ws'); - const wss = new ws.Server({ + const wss = new ws.WebSocketServer({ noServer: true, path: '/wsopen' }); webserver.on('upgrade', (request: any, socket: any, head: any) => { - // console.debug("connection upgrade ----> " + request.url); + console.debug("connection upgrade ----> " + request.url); const { pathname } = parse(request.url); if (pathname === '/wsopen') { wss.handleUpgrade(request, socket, head, (websocket: any) => { wss.emit('connection', websocket, request); }); - } else { - socket.destroy(); } }); webserver.on('error', (error) => { @@ -367,6 +365,7 @@ class Server { }); + this.app.modules.onWebSocketServer(webserver); } initialize() { diff --git a/lib/saito/modules.ts b/lib/saito/modules.ts index 6f3de9b77a..3e0ed73b23 100644 --- a/lib/saito/modules.ts +++ b/lib/saito/modules.ts @@ -3,6 +3,8 @@ import Peer from './peer'; import Transaction from './transaction'; import path from 'path'; import fs from 'fs'; +import ws from 'ws'; +import { parse } from 'url'; class Mods { @@ -126,8 +128,8 @@ class Mods { } } - } catch (err) { } - + } catch (err) { + } for (let iii = 0; iii < this.mods.length; iii++) { @@ -283,11 +285,11 @@ class Mods { // // ... setup moderation / filter functions // - for (let xmod of this.app.modules.respondTo('saito-moderation-app')) { - this.app_filter_func.push(xmod.respondTo('saito-moderation-app').filter_func); + for (let xmod of this.app.modules.respondTo('saito-moderation-app')) { + this.app_filter_func.push(xmod.respondTo('saito-moderation-app').filter_func); } - for (let xmod of this.app.modules.respondTo('saito-moderation-core')) { - this.core_filter_func.push(xmod.respondTo('saito-moderation-core').filter_func); + for (let xmod of this.app.modules.respondTo('saito-moderation-core')) { + this.core_filter_func.push(xmod.respondTo('saito-moderation-core').filter_func); } // @@ -311,7 +313,7 @@ class Mods { 'handshake_complete', async (peerIndex: bigint) => { - if (this.app.BROWSER){ + if (this.app.BROWSER) { // broadcasts my keylist to other peers await this.app.wallet.setKeyList(this.app.keychain.returnWatchedPublicKeys()); } @@ -372,16 +374,18 @@ class Mods { // // 1 = permit, -1 = do not permit // - moderateModule(tx=null, mod=null) { + moderateModule(tx = null, mod = null) { - if (mod == null || tx == null) { return 0; } + if (mod == null || tx == null) { + return 0; + } for (let z = 0; z < this.app_filter_func.length; z++) { let permit_through = this.app_filter_func[z](mod, tx); - if (permit_through == 1) { + if (permit_through == 1) { return 1; } - if (permit_through == -1) { + if (permit_through == -1) { return -1; } } @@ -394,16 +398,18 @@ class Mods { // // 1 = permit, -1 = do not permit // - moderateCore(tx=null) { + moderateCore(tx = null) { - if (tx == null) { return 0; } + if (tx == null) { + return 0; + } for (let z = 0; z < this.core_filter_func.length; z++) { let permit_through = this.core_filter_func[z](tx); - if (permit_through == 1) { + if (permit_through == 1) { return 1; } - if (permit_through == -1) { + if (permit_through == -1) { return -1; } } @@ -412,14 +418,13 @@ class Mods { } - - moderateAddress(publickey="") { + moderateAddress(publickey = '') { let newtx = new Transaction(); newtx.addFrom(publickey); return this.moderate(newtx); } - moderate(tx=null, app="") { + moderate(tx = null, app = '') { let permit_through = 0; @@ -427,10 +432,14 @@ class Mods { // if there is a relevant app-filter-function, respect it // for (let i = 0; i < this.mods.length; i++) { - if (this.mods[i].name == app || app == "*") { + if (this.mods[i].name == app || app == '*') { permit_through = this.moderateModule(tx, this.mods[i]); - if (permit_through == -1) { return -1; } - if (permit_through == 1) { return 1; } + if (permit_through == -1) { + return -1; + } + if (permit_through == 1) { + return 1; + } } } @@ -439,9 +448,13 @@ class Mods { // permit_through = this.moderateCore(tx); - if (permit_through == -1) { return -1; } - if (permit_through == 1) { return 1; } - + if (permit_through == -1) { + return -1; + } + if (permit_through == 1) { + return 1; + } + // // seems OK if we made it this far // @@ -456,7 +469,7 @@ class Mods { await this.mods[icb].render(this.app, this.mods[icb]); } } - this.app.connection.emit("saito-render-complete"); + this.app.connection.emit('saito-render-complete'); return null; } @@ -697,6 +710,39 @@ class Mods { } } + async onWebSocketServer(webserver) { + for (let i = 0; i < this.mods.length; i++) { + let mod = this.mods[i]; + let path = mod.getWebsocketPath(); + if (!path) { + continue; + } + console.log('creating websocket server for module :' + mod.name + " on path : "+path); + let wss = new ws.WebSocketServer({ + noServer: true, + // todo : check if the path is already being used or reserved? + path: "/"+path + }); + webserver.on('upgrade', (request: any, socket: any, head: any) => { + console.debug("connection on module : "+mod.name+" upgrade ----> " + request.url); + const parsedUrl = parse(request.url); + const pathname = parsedUrl.pathname; + const pathParts = pathname.split('/').filter(Boolean); + const subdirectory = pathParts.length > 0 ? pathParts[0] : null; + console.log(subdirectory + " - " + path); + if (subdirectory === path) { + console.log('inside handleUpgrade'); + wss.handleUpgrade(request, socket, head, (websocket: any) => { + console.log("handling upgrade ///"); + wss.emit('connection', websocket, request); + }); + } + }); + + mod.onWebSocketServer(wss); + } + } + /* async getBuildNumber() { for (let i = 0; i < this.mods.length; i++) { diff --git a/lib/templates/modtemplate.js b/lib/templates/modtemplate.js index 45a55f3484..b8faf09d5f 100644 --- a/lib/templates/modtemplate.js +++ b/lib/templates/modtemplate.js @@ -37,7 +37,7 @@ class ModTemplate { midnight: 'fa-solid fa-moon', milquetoast: 'fa-solid fa-cow', sangre3000: 'fa-solid fa-droplet-slash' - + }; this.processedTxs = {}; @@ -245,8 +245,8 @@ class ModTemplate { return 'Unknown Module'; } - returnTitle(){ - if (this.title){ + returnTitle() { + if (this.title) { return this.title; } return this.returnName(); @@ -264,12 +264,14 @@ class ModTemplate { return false; } - loadSettings() {} + loadSettings() { + } // // INITIALIZE HTML (deprecated by render(app)) // - async initializeHTML(app) {} + async initializeHTML(app) { + } // // ATTACH EVENTS (deprecated by render(app)) @@ -278,7 +280,8 @@ class ModTemplate { // DOM, allowing us to incorporate the web applications to our own // internal functions and send and receive transactions natively. // - attachEvents(app) {} + attachEvents(app) { + } // // LOAD FROM ARCHIVES @@ -314,7 +317,8 @@ class ModTemplate { // in those transcations can also subscribe to those confirmations by // by using shouldAffixCallbackToModule. // - async onConfirmation(blk, tx, confnum) {} + async onConfirmation(blk, tx, confnum) { + } // // some UI elements may provide special display options for modules which @@ -336,7 +340,8 @@ class ModTemplate { // this is where the most important code in your module should go, // listening to requests that come in over the blockchain and replying. // - onNewBlock(blk, lc) {} + onNewBlock(blk, lc) { + } // // @@ -349,7 +354,8 @@ class ModTemplate { // and then it is run a second time setting the LC to 1 for all of the // blocks that are moved (back?) into the longest_chain // - onChainReorganization(block_id, block_hash, lc, pos) {} + onChainReorganization(block_id, block_hash, lc, pos) { + } // // @@ -386,7 +392,8 @@ class ModTemplate { // fetch service-level data like DNS information instead of having to blindly // guess or manually examine their peers. // - async onPeerServiceUp(app, peer, service) {} + async onPeerServiceUp(app, peer, service) { + } // // @@ -394,21 +401,24 @@ class ModTemplate { // ON ARCHIVE HANDSHAKE COMPLETE // // this function runs when a node completes its handshake with a peer that offers archiving services - onArchiveHandshakeComplete(app, peer) {} + onArchiveHandshakeComplete(app, peer) { + } // // // ON CONNECTION STABLE // // this function runs "connect" event - onConnectionStable(app, peer) {} + onConnectionStable(app, peer) { + } // // // ON CONNECTION UNSTABLE // // this function runs "disconnect" event - onConnectionUnstable(app, peer) {} + onConnectionUnstable(app, peer) { + } // // SHOULD AFFIX CALLBACK TO MODULE @@ -474,7 +484,8 @@ class ModTemplate { // blockchain syncing. It will be triggered on startup and with // every additional block added. // - updateBlockchainSync(app, current, target) {} + updateBlockchainSync(app, current, target) { + } ///////////////////////// // MODULE INTERACTIONS // @@ -576,7 +587,8 @@ class ModTemplate { // data out into this function, which can be overridden as needed in // order to // - receiveEvent(eventname, data) {} + receiveEvent(eventname, data) { + } // // DEPRECATED -- port to app.connection.emit('event', {}); @@ -741,7 +753,7 @@ class ModTemplate { return this.app.network.sendRequestAsTransaction( message.request, message.data, - function (res) { + function(res) { return mycallback(res); } ); @@ -749,7 +761,7 @@ class ModTemplate { return this.app.network.sendRequestAsTransaction( message.request, message.data, - function (res) { + function(res) { return mycallback(res); }, peer.peerIndex @@ -796,7 +808,8 @@ class ModTemplate { async sendPeerRequestWithServiceFilter( servicename, msg, - success_callback = (res) => {} + success_callback = (res) => { + } ) { this.sendPeerRequestWithFilter( () => { @@ -844,7 +857,7 @@ class ModTemplate { this.app.network.sendRequestAsTransaction( message.request, message.data, - function (res) { + function(res) { if (success_callback != null) { success_callback(res); } @@ -866,14 +879,14 @@ class ModTemplate { return this.app.network.sendRequestAsTransaction( message.request, message.data, - function (res) { + function(res) { //JSON.stringify("callback data1: " + JSON.stringify(res)); return mycallback(res); } ); } - isSlug(slug){ + isSlug(slug) { return (slug == this.returnSlug()); } @@ -900,17 +913,20 @@ class ModTemplate { } } - handleUrlParams(urlParams) {} + handleUrlParams(urlParams) { + } showAlert() { this.alerts++; try { let qs = '#' + this.returnSlug() + ' > .redicon'; document.querySelector(qs).style.display = 'block'; - } catch (err) {} + } catch (err) { + } } - attachMeta() {} + attachMeta() { + } attachStyleSheets() { if (this.stylesheetAdded === true) return; @@ -924,7 +940,8 @@ class ModTemplate { ) { should_attach_sheet = false; } - } catch (err) {} + } catch (err) { + } }); if (should_attach_sheet) { @@ -953,7 +970,8 @@ class ModTemplate { if (el.attributes.src.nodeValue === script) { script_attached = true; } - } catch (err) {} + } catch (err) { + } }); scriptCount++; if (!script_attached) { @@ -991,7 +1009,8 @@ class ModTemplate { if (el.attributes.src.nodeValue === script) { script_attached = true; } - } catch (err) {} + } catch (err) { + } }); if (!script_attached) { const s = document.createElement('script'); @@ -1012,7 +1031,8 @@ class ModTemplate { this.scriptsAdded = false; } - attachMeta(app) {} + attachMeta(app) { + } removeStyleSheets(app) { this.stylesheets.forEach((stylesheet) => { @@ -1023,7 +1043,8 @@ class ModTemplate { this.stylesheetAdded = false; } - removeMeta() {} + removeMeta() { + } removeEvents() { this.eventListeners.forEach((eventListener) => { @@ -1059,15 +1080,15 @@ class ModTemplate {
Auto close in ${Math.ceil( - time / 1000 - )}s
+ time / 1000 + )}s

${warningTitle}

${warningText}

+ warningText.length == 0 + ? 'style=\'flex:1;\'' + : 'style=\'flex:2;\'' + }>${warningText}

`; let overlay_self = this.overlay; @@ -1083,7 +1104,8 @@ class ModTemplate { try { document.getElementById('clock_number').innerHTML = Math.ceil(time / 1000); - } catch (err) {} + } catch (err) { + } }, 250); } @@ -1102,15 +1124,28 @@ class ModTemplate { } hasSeenTransaction(tx) { - let hashed_data = this.name + tx.signature; + let hashed_data = this.name + tx.signature; - if (this.processedTxs[hashed_data]) { - return true; + if (this.processedTxs[hashed_data]) { + return true; + } + this.processedTxs[hashed_data] = true; + + return false; } - this.processedTxs[hashed_data] = true; - return false; -} + getWebsocketPath() { + return ''; + } + + onWebSocketServer(wss) { + // wss.on('connection', (socket, request) => { + // socket.on('message', (msg) => { + // }); + // socket.on('close', () => {}); + // socket.on('error', (err) => {}); + // }); + } } diff --git a/mods/limbo/lib/lite-dream-controls.js b/mods/limbo/lib/lite-dream-controls.js index 03652bf3e5..6cbae85f2a 100644 --- a/mods/limbo/lib/lite-dream-controls.js +++ b/mods/limbo/lib/lite-dream-controls.js @@ -138,6 +138,10 @@ class DreamControls{ //Tell PeerManager to pause streams for green room this.app.connection.emit('limbo-toggle-audio'); this.app.connection.emit('limbo-toggle-video'); + + if (!document.querySelector('.dream-controls-menu-item')) { + this.app.connection.emit('saito-limbo-add-yt-icon'); + } } @@ -270,10 +274,6 @@ class DreamControls{ //Only necessary for first click but doesn't hurt to have this.startTimer(); // Start timer e.currentTarget.classList.remove("click-me"); - - if (!document.querySelector('.dream-controls-menu-item')) { - this.app.connection.emit('saito-limbo-add-yt-icon'); - } } } diff --git a/mods/limbo/web/css/limbo-base.css b/mods/limbo/web/css/limbo-base.css index 8cb31cbc1d..7cd4c1edb7 100644 --- a/mods/limbo/web/css/limbo-base.css +++ b/mods/limbo/web/css/limbo-base.css @@ -538,4 +538,10 @@ video#local.noflip{ .yt-active > i { animation: pulsate 2s ease-in-out infinite !important; +} + +.yt-stream-type { + display: flex; + gap: 1.5rem; + align-items: center; } \ No newline at end of file diff --git a/mods/spam/spam.js b/mods/spam/spam.js index 29959297f3..7d93364d7d 100644 --- a/mods/spam/spam.js +++ b/mods/spam/spam.js @@ -30,9 +30,9 @@ class Spam extends ModTemplate { this.styles = ['/spam/style.css', '/saito/saito.css']; } if (this.app.BROWSER == 0) { - setInterval(() => { - this.nodeSpamLoop(app, this); - }, 13000); + // setInterval(() => { + // this.nodeSpamLoop(app, this); + // }, 13000); } } diff --git a/mods/youtube-client/lib/yt-client-init-stream.js b/mods/youtube-client/lib/yt-client-init-stream.js index 2983aab34d..ab84594549 100644 --- a/mods/youtube-client/lib/yt-client-init-stream.js +++ b/mods/youtube-client/lib/yt-client-init-stream.js @@ -18,10 +18,25 @@ class YoutubeInitStream{ if (document.getElementById("yt-stream-btn")){ document.getElementById("yt-stream-btn").onclick = (e) => { let stream_key = document.getElementById("yt-stream-identifier")?.value; - + let stream_type = document.querySelector('input[name=stream_type]:checked').value;; + if (stream_key != "") { - this_self.app.connection.emit("saito-yt-start-stream", {stream_key: stream_key}); + + console.log({ + stream_key: stream_key, + stream_type: stream_type + }); + + this_self.app.connection.emit("saito-yt-start-stream", { + stream_key: stream_key, + stream_type: stream_type + }); this_self.overlay.close(); + + this_self.app.options.youtube = {}; + this_self.app.options.youtube.stream_key = stream_key; + + this_self.app.storage.saveOptions(); } else { salert("Please provide a valid stream key"); } diff --git a/mods/youtube-client/lib/yt-client-init-stream.template.js b/mods/youtube-client/lib/yt-client-init-stream.template.js index f9604e220e..25bc0090ff 100644 --- a/mods/youtube-client/lib/yt-client-init-stream.template.js +++ b/mods/youtube-client/lib/yt-client-init-stream.template.js @@ -1,9 +1,23 @@ module.exports = YoutubeInitStreamTemplate = (app,mod) => { + if (app.options?.youtube?.stream_key != null) { + console.log("previous stream key:", app.options.youtube.stream_key); + } + let html = `
Youtube Live
- + + +
+ +
Main stream
+ +
Backup stream
+
+
Go Live
`; diff --git a/mods/youtube-client/youtube-client.js b/mods/youtube-client/youtube-client.js index d0bc04b0c9..0ae2f9ad50 100644 --- a/mods/youtube-client/youtube-client.js +++ b/mods/youtube-client/youtube-client.js @@ -16,15 +16,21 @@ class YoutubeClient extends ModTemplate { this.styles = ['/youtube-client/style.css', '/saito/saito.css']; this.stream_key = ''; + this.stream_type = 'main'; + this.stream_url = { + 'main': "rtmp://a.rtmp.youtube.com/live2", + 'backup': "rtmp://b.rtmp.youtube.com/live2?backup=1" + }; this.stream_status = false; this.ws = null; this.mediaRecorder = null; this.icon_id = ''; this.combined_stream = null; - this.app.connection.on('saito-yt-start-stream', (obj = {}) => { + this.app.connection.on('saito-yt-start-stream', async (obj = {}) => { this.stream_key = obj.stream_key; - this.startStream(); + this.stream_type = obj.stream_type; + await this.startStream(); this.startStreamStatus(); }); @@ -75,10 +81,10 @@ class YoutubeClient extends ModTemplate { } // respondTo - startStream(){ + async startStream(){ let this_self = this; - let mediaStream = this.getStreamData(); + let mediaStream = await this.getStreamData(); console.log("mediaStream:", mediaStream); if (mediaStream == false) { @@ -87,8 +93,8 @@ class YoutubeClient extends ModTemplate { const ws_url = window.location.protocol.replace('http', 'ws') + '//' + // http: -> ws:, https: -> wss: (window.location.hostname) + this.getPort() + - '/rtmp/' + - encodeURIComponent(`rtmp://b.rtmp.youtube.com/live2/${this_self.stream_key}`); + '/encoder?url=' + + encodeURIComponent(`${this.stream_url[this.stream_type]}/${this_self.stream_key}`); console.log('url:', ws_url); this_self.ws = new WebSocket(ws_url,"echo-protocol"); @@ -141,29 +147,44 @@ class YoutubeClient extends ModTemplate { let this_mod = this; } - getStreamData() { + async getStreamData() { let mods = this.app.modules.mods; for (let i = 0; i < mods.length; i++) { if (typeof mods[i].slug != "undefined") { if (mods[i].slug == "swarmcast") { let limbo = mods[i]; console.log('limbo mod: ', limbo); + + if (limbo.combinedStream == null) { + let options = { + identifier: "Swarmcast", + description: limbo.description + } + console.log("options:",options); + await limbo.getStream(options); + + console.log('limbo mod after: ', limbo); + } + return limbo.combinedStream; } } } + + + return false; } getPort() { // 44344 - test, prod // 3000 - local dev - let port = ':44344'; + let port = ''; let protocol = this.app.browser.protocol; console.log('protocol:', protocol); if (protocol == 'http:') { - port = ':3000'; + port = `:${this.app.browser.port}`; } console.log('port:', port); diff --git a/mods/youtube-server/youtube-server.js b/mods/youtube-server/youtube-server.js index 052c38c4c8..b6d98602cd 100644 --- a/mods/youtube-server/youtube-server.js +++ b/mods/youtube-server/youtube-server.js @@ -1,140 +1,254 @@ const ModTemplate = require('./../../lib/templates/modtemplate'); +//const child_process = require('child_process'); const PeerService = require('saito-js/lib/peer_service').default; class YoutubeServer extends ModTemplate { - constructor(app) { - super(app); - - this.app = app; - this.slug = 'youtube-server'; - this.name = 'youtube-server'; - this.description = 'Server for encoding video for YT stream via ffmpeg with WebSocket'; - this.categories = 'Utilities Communications'; - this.class = 'utility'; - this.publickey = ''; - this.styles = ['/youtube-server/style.css', '/saito/saito.css']; - - return this; - } - - initialize(app) { - super.initialize(app); - if (this.browser_active) { - this.styles = ['/youtube-server/style.css', '/saito/saito.css']; - } - if (this.app.BROWSER == 0) { - } - } - - async render() { - if (!this.browser_active) { - return; - } - let this_mod = this; - } - - initializeWebSocketServer() { - const child_process = require('child_process'); - const WebSocketServer = require('ws').Server; - const wss = new WebSocketServer({ port: 3000 }); - - wss.on('connection', (ws, req) => { - // Ensure that the URL starts with '/rtmp/', and extract the target RTMP URL. - let match; - if ( !(match = req.url.match(/^\/rtmp\/(.*)$/)) ) { - ws.terminate(); // No match, reject the connection. - return; - } - - const rtmpUrl = decodeURIComponent(match[1]); - console.log('Target RTMP URL:', rtmpUrl); - - // Launch FFmpeg to handle all appropriate transcoding, muxing, and RTMP. - // If 'ffmpeg' isn't in your path, specify the full path to the ffmpeg binary. - const ffmpeg = child_process.spawn('ffmpeg', [ - // FFmpeg will read input video from STDIN - '-i', '-', - - // If we're encoding H.264 in-browser, we can set the video codec to 'copy' - // so that we don't waste any CPU and quality with unnecessary transcoding. - // If the browser doesn't support H.264, set the video codec to 'libx264' - // or similar to transcode it to H.264 here on the server. - '-vcodec', 'libx264', - - // AAC audio is required for Live. No browser currently supports - // encoding AAC, so we must transcode the audio to AAC here on the server. - - '-b:a', '160k', - - '-ab', '128k', - - '-ac', '2', - - '-af', "adelay=1|1", - - '-async', '1', - -// '-acodec', 'copy', - - '-c:a', 'aac', - - '-ar', '44100', - - '-r', '25', - - '-s', '1920x1080', - - '-vb', '660k', - - // FLV is the container format used in conjunction with RTMP - '-f', 'flv', - - // The output RTMP URL. - // For debugging, you could set this to a filename like 'test.flv', and play - // the resulting file with VLC. Please also read the security considerations - // later on in this tutorial. - rtmpUrl - ]); - - // If FFmpeg stops for any reason, close the WebSocket connection. - ffmpeg.on('close', (code, signal) => { - console.log('FFmpeg child process closed, code ' + code + ', signal ' + signal); - ws.terminate(); - }); - - // Handle STDIN pipe errors by logging to the console. - // These errors most commonly occur when FFmpeg closes and there is still - // data to write. If left unhandled, the server will crash. - ffmpeg.stdin.on('error', (e) => { - console.log('FFmpeg STDIN Error', e); - }); - - // FFmpeg outputs all of its messages to STDERR. Let's log them to the console. - ffmpeg.stderr.on('data', (data) => { - console.log('FFmpeg STDERR:', data.toString()); - }); - - // When data comes in from the WebSocket, write it to FFmpeg's STDIN. - ws.on('message', (msg) => { - console.log('DATA', msg); - ffmpeg.stdin.write(msg); - }); - - // If the client disconnects, stop FFmpeg. - ws.on('close', (e) => { - ffmpeg.kill('SIGINT'); - }); - }); - - } - - webServer(app, expressapp, express) { - this.initializeWebSocketServer(); - let webdir = `${__dirname}/../../mods/${this.dirname}/web`; - expressapp.use("/" + encodeURI(this.returnSlug()), express.static(webdir)); - } - - + constructor(app) { + super(app); + + this.app = app; + this.slug = 'youtube-server'; + this.name = 'youtube-server'; + this.description = 'Server for encoding video for YT stream via ffmpeg with WebSocket'; + this.categories = 'Utilities Communications'; + this.class = 'utility'; + this.publickey = ''; + this.styles = ['/youtube-server/style.css', '/saito/saito.css']; + + return this; + } + + async initialize(app) { + await super.initialize(app); + if (this.browser_active) { + this.styles = ['/youtube-server/style.css', '/saito/saito.css']; + } + if (this.app.BROWSER == 0) { + } + } + + async render() { + if (!this.browser_active) { + return; + } + let this_mod = this; + } + + initializeWebSocketServer() { + const child_process = require('child_process'); + const WebSocketServer = require('ws').Server; + // const wss = new WebSocketServer({ port: 3000 }); + + wss.on('connection', (ws, req) => { + console.info('yt - on connection fired'); + // Ensure that the URL starts with '/rtmp/', and extract the target RTMP URL. + let match; + if (!(match = req.url.match(/^\/rtmp\/(.*)$/))) { + console.log("aaa terminating..."); + ws.terminate(); // No match, reject the connection. + return; + } + + const rtmpUrl = decodeURIComponent(match[1]); + console.log('Target RTMP URL:', rtmpUrl); + + // Launch FFmpeg to handle all appropriate transcoding, muxing, and RTMP. + // If 'ffmpeg' isn't in your path, specify the full path to the ffmpeg binary. + const ffmpeg = child_process.spawn('ffmpeg', [ + // FFmpeg will read input video from STDIN + '-i', + '-', + + // If we're encoding H.264 in-browser, we can set the video codec to 'copy' + // so that we don't waste any CPU and quality with unnecessary transcoding. + // If the browser doesn't support H.264, set the video codec to 'libx264' + // or similar to transcode it to H.264 here on the server. + '-vcodec', + 'libx264', + + // AAC audio is required for Live. No browser currently supports + // encoding AAC, so we must transcode the audio to AAC here on the server. + + '-ab', + '128k', + + '-ac', + '2', + + '-ar', + '44100', + + '-r', + '25', + + '-s', + '720x420', + + '-vb', + '660k', + + // FLV is the container format used in conjunction with RTMP + '-f', + 'flv', + + // The output RTMP URL. + // For debugging, you could set this to a filename like 'test.flv', and play + // the resulting file with VLC. Please also read the security considerations + // later on in this tutorial. + rtmpUrl + ]); + + // If FFmpeg stops for any reason, close the WebSocket connection. + ffmpeg.on('close', (code, signal) => { + console.log('FFmpeg child process closed, code ' + code + ', signal ' + signal); + ws.terminate(); + }); + + // Handle STDIN pipe errors by logging to the console. + // These errors most commonly occur when FFmpeg closes and there is still + // data to write. If left unhandled, the server will crash. + ffmpeg.stdin.on('error', (e) => { + console.log('FFmpeg STDIN Error', e); + }); + + // FFmpeg outputs all of its messages to STDERR. Let's log them to the console. + ffmpeg.stderr.on('data', (data) => { + console.log('FFmpeg STDERR:', data.toString()); + }); + + // When data comes in from the WebSocket, write it to FFmpeg's STDIN. + ws.on('message', (msg) => { + console.log('DATA', msg); + ffmpeg.stdin.write(msg); + }); + + // If the client disconnects, stop FFmpeg. + ws.on('close', (e) => { + ffmpeg.kill('SIGINT'); + }); + }); + } + + webServer(app, expressapp, express) { + // this.initializeWebSocketServer(); + let webdir = `${__dirname}/../../mods/${this.dirname}/web`; + expressapp.use('/' + encodeURI(this.returnSlug()), express.static(webdir)); + } + + getWebsocketPath() { + return 'encoder'; + } + + async onWebSocketServer(wss) { + const child_process = require('child_process'); + console.log('youtube on websocket server'); + await super.onWebSocketServer(wss); + wss.on('connection', (ws, req) => { + console.log('youtube server got connection'); + // Ensure that the URL starts with '/rtmp/', and extract the target RTMP URL. + let match; + + let rtmp_url = (req.url).split("url=")[1]; + console.log("wss request url:", rtmp_url); + + if (rtmp_url == null) { + console.log('terminating youtube connection'); + ws.terminate(); // No match, reject the connection. + return; + } + + const rtmpUrl = decodeURIComponent(rtmp_url); + console.log('Target RTMP URL:', rtmpUrl); + + // Launch FFmpeg to handle all appropriate transcoding, muxing, and RTMP. + // If 'ffmpeg' isn't in your path, specify the full path to the ffmpeg binary. + const ffmpeg = child_process.spawn('ffmpeg', [ + // FFmpeg will read input video from STDIN + '-i', + '-', + + // If we're encoding H.264 in-browser, we can set the video codec to 'copy' + // so that we don't waste any CPU and quality with unnecessary transcoding. + // If the browser doesn't support H.264, set the video codec to 'libx264' + // or similar to transcode it to H.264 here on the server. + '-vcodec', + 'libx264', + + // AAC audio is required for Live. No browser currently supports + // encoding AAC, so we must transcode the audio to AAC here on the server. + + '-ab', + '128k', + + '-ac', + '2', + + '-ar', + '44100', + + '-r', + '25', + + '-s', + '720x420', + + '-vb', + '660k', + + // FLV is the container format used in conjunction with RTMP + '-f', + 'flv', + + // The output RTMP URL. + // For debugging, you could set this to a filename like 'test.flv', and play + // the resulting file with VLC. Please also read the security considerations + // later on in this tutorial. + rtmpUrl + ]); + + // If FFmpeg stops for any reason, close the WebSocket connection. + ffmpeg.on('close', (code, signal) => { + console.log('FFmpeg child process closed, code ' + code + ', signal ' + signal); + ws.terminate(); + }); + + // Handle STDIN pipe errors by logging to the console. + // These errors most commonly occur when FFmpeg closes and there is still + // data to write. If left unhandled, the server will crash. + ffmpeg.stdin.on('error', (e) => { + console.log('FFmpeg STDIN Error', e); + }); + + // FFmpeg outputs all of its messages to STDERR. Let's log them to the console. + ffmpeg.stderr.on('data', (data) => { + console.log('FFmpeg STDERR:', data.toString()); + }); + + // When data comes in from the WebSocket, write it to FFmpeg's STDIN. + ws.on('message', (msg) => { + console.log('DATA', msg); + ffmpeg.stdin.write(msg); + }); + + // If the client disconnects, stop FFmpeg. + ws.on('close', (e) => { + console.log("youtube server socket closed"); + ffmpeg.kill('SIGINT'); + }); + }); + wss.on('close', (e) => { + console.log('youtube connection closed : ', e); + }); + wss.on('error', (e) => { + console.error(e); + }); + wss.on('disconnect', (code, signal) => { + console.log('youtube connection disconnected'); + }); + wss.on('terminate', (code, signal) => { + console.log('terminating youtube connection'); + }) + } } module.exports = YoutubeServer;