diff options
| author | Jibon Costa <jiboncosta57@gmail.com> | 2020-03-28 16:38:08 +0600 |
|---|---|---|
| committer | Jibon Costa <jiboncosta57@gmail.com> | 2020-03-28 16:38:08 +0600 |
| commit | b15978e75643ce6c216e81f0e0efc11aab0be084 (patch) | |
| tree | 41f64eda3549609ecba6a44fe4b4f9d7780607e6 | |
| parent | 81e67d3be90c46910123b30bc07cab3688c041fb (diff) | |
| download | bbb-recorder-b15978e75643ce6c216e81f0e0efc11aab0be084.tar.gz bbb-recorder-b15978e75643ce6c216e81f0e0efc11aab0be084.zip | |
Live RTMP broadcasting
| -rw-r--r-- | README.md | 30 | ||||
| -rw-r--r-- | background.js | 22 | ||||
| -rw-r--r-- | ffmpegServer.js | 83 | ||||
| -rw-r--r-- | liveRTMP.js | 99 | ||||
| -rw-r--r-- | package.json | 3 |
5 files changed, 231 insertions, 6 deletions
@@ -46,6 +46,26 @@ You can also use `liveJoin.js` to live join meeting as a recorder & perform reco node liveJoin.js "https://BBB_HOST/bigbluebutton/api/join?meetingId=MEETING_ID...." liveRecord.webm 10 true ``` +**Live RTMP broadcasting.(Experimental)** + +Sometime you may want to broadcast meeting via RTMP. I did some experiment on it & got success but not 100%. To test you can use `ffmpegServer.js` to run websocket server & `liveRTMP.js` to join the meeting. You'll have to edit `rtmpUrl` from `ffmpegServer.js` & port (if need). Then add websocket value as `ffmpegServer` in `liveRTMP.js` file. + +1) Frist run websocket server by `node ffmpegServer.js` +2) Then in another terminal tab + +```sh +node liveJoin.js "https://BBB_HOST/bigbluebutton/api/join?meetingId=MEETING_ID...." +``` +You can also set duration like this too otherwise it will close after meeting end or kickout: + +```sh +node liveJoin.js "https://BBB_HOST/bigbluebutton/api/join?meetingId=MEETING_ID...." 20 +``` + +Check the process of websocket server, `ffmpeg` should start sending data to RTMP server. + +**Note:** +If you do nothing in meeting room that time `ffmpeg` may exit with error. Actually I don't have much experience on `ffmpeg` to resolve those problems. Please contribute your experience. ### How it will work? When you will run the command that time `chromium` browser will be open in background & visit the link & perform screen recording. So, if you have set 10 seconds then it will record 10 seconds only. Later it will give you file as webm or mp4. @@ -53,9 +73,13 @@ When you will run the command that time `chromium` browser will be open in backg **Note: It will use extra CPU to process chrome & ffmpeg.** +### Looking for Bigbluebutton shared hosting? + +We are offering cheaper [Bigbluebutton shared hosting](https://www.mynaparrot.com/classroom) or Bigbluebutton insallation/configuration service. You can send me email jibon[@]mynaparrot.com + -Thanks to [@muralikg](https://github.com/muralikg/puppetcam). Everything was copied from there & I did some adjustment. +### Thanks to +[puppetcam](https://github.com/muralikg/puppetcam). Most of the parts were copied from there. -### Looking for Bigbluebutton shared hosting? -We are offering cheaper [Bigbluebutton shared hosting](https://www.mynaparrot.com/classroom) or Bigbluebutton insallation/configuration service. You can send me email jibon[@]mynaparrot.com +[Canvas-Streaming-Example](https://github.com/fbsamples/Canvas-Streaming-Example)
\ No newline at end of file diff --git a/background.js b/background.js index cb2fb8b..253e81e 100644 --- a/background.js +++ b/background.js @@ -2,6 +2,8 @@ let recorder = null; let filename = null; +let ws; +let liveSteam = false; chrome.runtime.onConnect.addListener(port => { port.onMessage.addListener(msg => { @@ -10,11 +12,20 @@ chrome.runtime.onConnect.addListener(port => { case 'SET_EXPORT_PATH': filename = msg.filename break + case 'FFMPEG_SERVER': + let ffmpegServer = msg.ffmpegServer + ws = new WebSocket(ffmpegServer); + liveSteam = true; + break case 'REC_STOP': recorder.stop() break case 'REC_START': - recorder.start(); + if(liveSteam){ + recorder.start(1000); + }else{ + recorder.start(); + } break case 'REC_CLIENT_PLAY': if(recorder){ @@ -42,7 +53,7 @@ chrome.runtime.onConnect.addListener(port => { } } }, stream => { - var chunks=[]; + var chunks = []; recorder = new MediaRecorder(stream, { videoBitsPerSecond: 2500000, ignoreMutedMedia: true, @@ -51,10 +62,17 @@ chrome.runtime.onConnect.addListener(port => { recorder.ondataavailable = function (event) { if (event.data.size > 0) { chunks.push(event.data); + if(liveSteam){ + ws.send(event.data); + } } }; recorder.onstop = function () { + if(liveSteam){ + ws.close(); + } + var superBuffer = new Blob(chunks, { type: 'video/webm' }); diff --git a/ffmpegServer.js b/ffmpegServer.js new file mode 100644 index 0000000..0ab0e6a --- /dev/null +++ b/ffmpegServer.js @@ -0,0 +1,83 @@ +const child_process = require('child_process'); +const WebSocketServer = require('ws').Server; +const http = require('http'); + +const server = http.createServer().listen(4000, () => { + console.log('Listening...'); +}); + +const wss = new WebSocketServer({ + server: server +}); + + +const rtmpUrl = "rtmp://a.rtmp.youtube.com/live2/MyKey"; + +wss.on('connection', function connection(ws, req) { + console.log('connection'); + + let auth; + + if ( !(auth = req.url.match(/^\/auth\/(.*)$/)) ) { + ws.terminate(); + return; + } + + if(auth[1] !== "mZFZN4yc"){ + ws.terminate(); + return; + } + + const ffmpeg = child_process.spawn('ffmpeg', [ + + // FFmpeg will read input video from STDIN + '-i', '-', + + // Chromium 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', + + //No browser currently supports encoding AAC, so we must transcode the audio to AAC here on the server. + '-acodec', 'aac', + + // FLV is the container format used in conjunction with RTMP + '-f', 'flv', + + '-max_muxing_queue_size', '99999', + + //'-vf', 'mpdecimate', '-vsync', 'vfr', + //'-vf', 'mpdecimate,setpts=N/FRAME_RATE/TB', + + // The output RTMP URL. + // For debugging, you could set this to a filename like 'test.flv', and play + // the resulting file with VLC. + 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(); + }); + + ffmpeg.stdin.on('error', (e) => { + console.log('FFmpeg STDIN Error', e); + }); + + 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'); + ws.terminate(); + }); + +});
\ No newline at end of file diff --git a/liveRTMP.js b/liveRTMP.js new file mode 100644 index 0000000..0fcbfef --- /dev/null +++ b/liveRTMP.js @@ -0,0 +1,99 @@ +const puppeteer = require('puppeteer'); +const Xvfb = require('xvfb'); +var exec = require('child_process').exec; +const fs = require('fs'); +const homedir = require('os').homedir(); +const ffmpegServer = "ws://localhost:4000/auth/mZFZN4yc"; + +var xvfb = new Xvfb({silent: true}); +var width = 1280; +var height = 720; +var options = { + headless: false, + args: [ + '--enable-usermedia-screen-capturing', + '--allow-http-screen-capture', + '--auto-select-desktop-capture-source=bbbrecorder', + '--load-extension=' + __dirname, + '--disable-extensions-except=' + __dirname, + '--disable-infobars', + '--no-sandbox', + '--shm-size=1gb', + '--disable-dev-shm-usage', + `--window-size=${width},${height}`, + ], +} + +async function main() { + try{ + xvfb.startSync() + var url = process.argv[2], + duration = process.argv[3], + exportname = 'liveMeeting.webm' + + if(!url){ url = 'http://tobiasahlin.com/spinkit/' } + //if(!duration){ duration = 10 } + + const browser = await puppeteer.launch(options) + const pages = await browser.pages() + + const page = pages[0] + + page.on('console', msg => { + var m = msg.text(); + console.log('PAGE LOG:', m) + }); + + await page._client.send('Emulation.clearDeviceMetricsOverride') + await page.goto(url, {waitUntil: 'networkidle2'}) + await page.setBypassCSP(true) + + await page.evaluate((serverAddress) => { + console.log("FFMPEG_SERVER"); + window.postMessage({type: 'FFMPEG_SERVER', ffmpegServer: serverAddress}, '*') + }, ffmpegServer) + + await page.waitForSelector('[aria-label="Listen only"]'); + await page.click('[aria-label="Listen only"]', {waitUntil: 'domcontentloaded'}); + + await page.waitForSelector('[id="chat-toggle-button"]'); + await page.click('[id="chat-toggle-button"]', {waitUntil: 'domcontentloaded'}); + await page.click('button[aria-label="Users and messages toggle"]', {waitUntil: 'domcontentloaded'}); + await page.$eval('[class^=navbar]', element => element.style.display = "none"); + + await page.$eval('.Toastify', element => element.style.display = "none"); + await page.waitForSelector('button[aria-label="Leave audio"]'); + await page.$eval('[class^=actionsbar] > [class^=center]', element => element.style.display = "none"); + + await page.evaluate((x) => { + console.log("REC_START"); + window.postMessage({type: 'REC_START'}, '*') + }) + + if(duration > 0){ + await page.waitFor((duration * 1000)) + }else{ + await page.waitForSelector('[class^=modal] > [class^=content] > button[description="Logs you out of the meeting"]', { + timeout: 0 + }); + } + + await page.evaluate(filename=>{ + window.postMessage({type: 'SET_EXPORT_PATH', filename: filename}, '*') + window.postMessage({type: 'REC_STOP'}, '*') + }, exportname) + + // Wait for download of webm to complete + await page.waitForSelector('html.downloadComplete', {timeout: 0}) + await page.close() + await browser.close() + xvfb.stopSync() + + fs.unlinkSync(homedir + "/Downloads/liveMeeting.webm"); + + }catch(err) { + console.log(err) + } +} + +main() diff --git a/package.json b/package.json index fecd5b0..32de7e1 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ ], "dependencies": { "puppeteer": "2.0.0", - "xvfb": "^0.2.3" + "xvfb": "^0.2.3", + "ws": "^7.2.3" } } |
