|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +const { PassThrough } = require('stream') |
| 4 | + |
| 5 | +const { RTCAudioSink, RTCVideoSink } = require('wrtc').nonstandard; |
| 6 | + |
| 7 | +const ffmpeg = require('fluent-ffmpeg') |
| 8 | +const { StreamInput } = require('fluent-ffmpeg-multistream') |
| 9 | +const fs = require('fs') |
| 10 | + |
| 11 | +const VIDEO_OUTPUT_SIZE = '320x240' |
| 12 | +const VIDEO_OUTPUT_FILE = './recording.mp4' |
| 13 | + |
| 14 | +let UID = 0; |
| 15 | + |
| 16 | +function beforeOffer(peerConnection) { |
| 17 | + const audioTransceiver = peerConnection.addTransceiver('audio'); |
| 18 | + const videoTransceiver = peerConnection.addTransceiver('video'); |
| 19 | + |
| 20 | + const audioSink = new RTCAudioSink(audioTransceiver.receiver.track); |
| 21 | + const videoSink = new RTCVideoSink(videoTransceiver.receiver.track); |
| 22 | + |
| 23 | + const streams = []; |
| 24 | + |
| 25 | + videoSink.addEventListener('frame', function({ frame: { width, height, data }}){ |
| 26 | + const size = width + 'x' + height; |
| 27 | + if(!streams[0] || (streams[0] && streams[0].size !== size)) { |
| 28 | + UID++; |
| 29 | + |
| 30 | + const stream = { |
| 31 | + recordPath: './recording-' + size + '-' + UID + '.mp4', |
| 32 | + size, |
| 33 | + video: new PassThrough(), |
| 34 | + audio: new PassThrough() |
| 35 | + }; |
| 36 | + |
| 37 | + const onAudioData = function({ samples: { buffer } }) { |
| 38 | + if(!stream.end) { |
| 39 | + stream.audio.push(Buffer.from(buffer)); |
| 40 | + } |
| 41 | + }; |
| 42 | + |
| 43 | + audioSink.addEventListener('data', onAudioData) |
| 44 | + |
| 45 | + stream.audio.on('end', ()=>{ |
| 46 | + audioSink.removeEventListener('data', onAudioData) |
| 47 | + }) |
| 48 | + |
| 49 | + streams.unshift(stream) |
| 50 | + |
| 51 | + streams.forEach(item=>{ |
| 52 | + if(item !== stream && !item.end) { |
| 53 | + item.end = true; |
| 54 | + if(item.audio) { |
| 55 | + item.audio.end(); |
| 56 | + } |
| 57 | + item.video.end(); |
| 58 | + } |
| 59 | + }) |
| 60 | + |
| 61 | + stream.proc = ffmpeg() |
| 62 | + .addInput((new StreamInput(stream.video)).url) |
| 63 | + .addInputOptions([ |
| 64 | + '-f', 'rawvideo', |
| 65 | + '-pix_fmt', 'yuv420p', |
| 66 | + '-s', stream.size, |
| 67 | + '-r', '30', |
| 68 | + ]) |
| 69 | + .addInput((new StreamInput(stream.audio)).url) |
| 70 | + .addInputOptions([ |
| 71 | + '-f s16le', |
| 72 | + '-ar 48k', |
| 73 | + '-ac 1', |
| 74 | + ]) |
| 75 | + .on('start', ()=>{ |
| 76 | + console.log('Start recording >> ', stream.recordPath) |
| 77 | + }) |
| 78 | + .on('end', ()=>{ |
| 79 | + stream.recordEnd = true; |
| 80 | + console.log('Stop recording >> ', stream.recordPath) |
| 81 | + }) |
| 82 | + .size(VIDEO_OUTPUT_SIZE) |
| 83 | + .output(stream.recordPath) |
| 84 | + |
| 85 | + stream.proc.run() |
| 86 | + } |
| 87 | + |
| 88 | + streams[0].video.push(Buffer.from(data)); |
| 89 | + }) |
| 90 | + |
| 91 | + const { close } = peerConnection; |
| 92 | + peerConnection.close = function() { |
| 93 | + audioSink.stop(); |
| 94 | + videoSink.stop(); |
| 95 | + |
| 96 | + streams.forEach(({ audio, video, end, proc, recordPath })=>{ |
| 97 | + if(!end) { |
| 98 | + if(audio) { |
| 99 | + audio.end(); |
| 100 | + } |
| 101 | + video.end(); |
| 102 | + } |
| 103 | + }) |
| 104 | + |
| 105 | + let totalEnd = 0; |
| 106 | + const timer = setInterval(()=>{ |
| 107 | + streams.forEach(stream=>{ |
| 108 | + if(stream.recordEnd) { |
| 109 | + totalEnd++; |
| 110 | + if(totalEnd === streams.length) { |
| 111 | + clearTimeout(timer); |
| 112 | + |
| 113 | + const mergeProc = ffmpeg() |
| 114 | + .on('start', ()=>{ |
| 115 | + console.log('Start merging into ' + VIDEO_OUTPUT_FILE); |
| 116 | + }) |
| 117 | + .on('end', ()=>{ |
| 118 | + streams.forEach(({ recordPath })=>{ |
| 119 | + fs.unlinkSync(recordPath); |
| 120 | + }) |
| 121 | + console.log('Merge end. You can play ' + VIDEO_OUTPUT_FILE); |
| 122 | + }); |
| 123 | + |
| 124 | + streams.forEach(({ recordPath })=>{ |
| 125 | + mergeProc.addInput(recordPath) |
| 126 | + }) |
| 127 | + |
| 128 | + mergeProc |
| 129 | + .output(VIDEO_OUTPUT_FILE) |
| 130 | + .run(); |
| 131 | + } |
| 132 | + } |
| 133 | + }) |
| 134 | + },1000) |
| 135 | + |
| 136 | + return close.apply(this, arguments); |
| 137 | + } |
| 138 | +} |
| 139 | + |
| 140 | +module.exports = { beforeOffer }; |
0 commit comments