From 584ee6f1b9978379e2776921b01e308685d43678 Mon Sep 17 00:00:00 2001 From: Alexey Nurgaliev Date: Mon, 6 Nov 2023 18:08:14 +0300 Subject: [PATCH] screen recording (#716) * screen recording * screen recording performance optimizations --- data/emulator.js | 128 +++++++++++++++++++++++++++++++++++++++++++ data/emulator.min.js | 2 +- data/loader.js | 1 + 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/data/emulator.js b/data/emulator.js index eca360f..5a7c3b5 100644 --- a/data/emulator.js +++ b/data/emulator.js @@ -1281,6 +1281,27 @@ class EmulatorJS { a.click(); hideMenu(); }); + + let screenMediaRecorder = null; + const startScreenRecording = addButton("Start screen recording", false, () => { + if (screenMediaRecorder !== null) { + screenMediaRecorder.stop(); + } + screenMediaRecorder = this.screenRecord(); + startScreenRecording.setAttribute("hidden", "hidden"); + stopScreenRecording.removeAttribute("hidden"); + hideMenu(); + }); + const stopScreenRecording = addButton("Stop screen recording", true, () => { + if (screenMediaRecorder !== null) { + screenMediaRecorder.stop(); + screenMediaRecorder = null; + } + startScreenRecording.removeAttribute("hidden"); + stopScreenRecording.setAttribute("hidden", "hidden"); + hideMenu(); + }); + const qSave = addButton("Quick Save", false, () => { const slot = this.settings['save-state-slot'] ? this.settings['save-state-slot'] : "1"; this.gameManager.quickSave(slot); @@ -1295,6 +1316,8 @@ class EmulatorJS { }); this.elements.contextMenu = { screenshot: screenshot, + startScreenRecording: startScreenRecording, + stopScreenRecording: stopScreenRecording, save: qSave, load: qLoad } @@ -1395,6 +1418,7 @@ class EmulatorJS { if (this.config.buttonOpts) { if (this.config.buttonOpts.screenshot === false) screenshot.setAttribute("hidden", ""); + if (this.config.buttonOpts.screenRecord === false) startScreenRecording.setAttribute("hidden", ""); if (this.config.buttonOpts.quickSave === false) qSave.setAttribute("hidden", ""); if (this.config.buttonOpts.quickLoad === false) qLoad.setAttribute("hidden", ""); } @@ -4786,5 +4810,109 @@ class EmulatorJS { cheatChanged(checked, code, index) { this.gameManager.setCheat(index, checked, code); } + + collectScreenRecordingMediaTracks(canvasEl, fps) { + let videoTrack = null; + const videoTracks = canvasEl.captureStream(fps).getVideoTracks(); + if (videoTracks.length !== 0) { + videoTrack = videoTracks[0]; + } else { + console.error('Unable to capture video stream'); + return null; + } + + let audioTrack = null; + if (window.AL && window.AL.currentCtx && window.AL.currentCtx.audioCtx) { + const alContext = window.AL.currentCtx; + const audioContext = alContext.audioCtx; + + const gainNodes = []; + for (let sourceIdx in alContext.sources) { + gainNodes.push(alContext.sources[sourceIdx].gain); + } + + const merger = audioContext.createChannelMerger(gainNodes.length); + gainNodes.forEach(node => node.connect(merger)); + + const destination = audioContext.createMediaStreamDestination(); + merger.connect(destination); + + const audioTracks = destination.stream.getAudioTracks(); + if (audioTracks.length !== 0) { + audioTrack = audioTracks[0]; + } + } + + const stream = new MediaStream(); + if (videoTrack && videoTrack.readyState === 'live') { + stream.addTrack(videoTrack); + } + if (audioTrack && audioTrack.readyState === 'live') { + stream.addTrack(audioTrack); + } + return stream; + } + + screenRecord() { + const captureScreenWidth= (this.config.screenRecording && (typeof this.config.screenRecording.width == "number")) ? this.config.screenRecording.width : 800; + const captureScreenHeight = (this.config.screenRecording && (typeof this.config.screenRecording.height == "number")) ? this.config.screenRecording.height : 600; + const captureFps = (this.config.screenRecording && (typeof this.config.screenRecording.fps == "number")) ? this.config.screenRecording.fps : 30; + const captureVideoBitrate = (this.config.screenRecording && (typeof this.config.screenRecording.videoBitrate == "number")) ? this.config.screenRecording.videoBitrate : 2 * 1024 * 1014; + const captureAudioBitrate = (this.config.screenRecording && (typeof this.config.screenRecording.audioBitrate == "number")) ? this.config.screenRecording.audioBitrate : 256 * 1024; + + const captureCanvas = document.createElement('canvas'); + captureCanvas.width = captureScreenWidth; + captureCanvas.height = captureScreenHeight; + captureCanvas.style.position = 'absolute'; + captureCanvas.style.top = '-999px'; + captureCanvas.style.bottom = '-999px'; + document.getElementsByTagName('body')[0].append(captureCanvas); + + const captureCtx = captureCanvas.getContext('2d', { alpha: false }); + captureCtx.fillStyle = '#000'; + + let animation = true; + + const drawNextFrame = () => { + const scaleX = captureScreenWidth / this.canvas.width; + const scaleY = captureScreenHeight / this.canvas.height; + const scale = Math.max(scaleY, scaleX); + const width = this.canvas.width * scale; + const height = this.canvas.height * scale; + const startX = (captureScreenWidth - width) / 2; + const startY = (captureScreenHeight - height) / 2; + captureCtx.drawImage(this.canvas, Math.round(startX), Math.round(startY), Math.round(width), Math.round(height)); + if (animation) { + requestAnimationFrame(drawNextFrame); + } + }; + requestAnimationFrame(drawNextFrame); + + const chunks = []; + const tracks = this.collectScreenRecordingMediaTracks(captureCanvas, captureFps); + const recorder = new MediaRecorder(tracks, { + videoBitsPerSecond: captureVideoBitrate, + audioBitsPerSecond: captureAudioBitrate, + }); + recorder.addEventListener('dataavailable', e => { + chunks.push(e.data); + }); + recorder.addEventListener('stop', () => { + const blob = new Blob(chunks); + const url = URL.createObjectURL(blob); + const date = new Date(); + const a = document.createElement('a'); + a.href = url; + a.download = this.getBaseFileName()+"-"+date.getMonth()+"-"+date.getDate()+"-"+date.getFullYear()+".webm"; + a.click(); + + animation = false; + captureCanvas.remove(); + }); + recorder.start(); + + return recorder; + } + } window.EmulatorJS = EmulatorJS; diff --git a/data/emulator.min.js b/data/emulator.min.js index cbd36e5..56d3633 100644 --- a/data/emulator.min.js +++ b/data/emulator.min.js @@ -1 +1 @@ -!function(){var e,t;e=window,t=function(){return n=[function(e,t,n){"use strict";n.r(t);function b(e,t){var n=t.x-e.x,t=t.y-e.y;return Math.sqrt(n*n+t*t)}function _(e){return e*(Math.PI/180)}function o(e){f.has(e)&&clearTimeout(f.get(e)),f.set(e,setTimeout(e,100))}function a(e,t,n){for(var i,o=t.split(/[ ,]+/g),a=0;athis.options.threshold){var r,l={};for(r in this.direction)this.direction.hasOwnProperty(r)&&(l[r]=this.direction[r]);var c={};for(r in this.direction={x:n,y:i,angle:t},e.direction=this.direction,l)l[r]===this.direction[r]&&(c[r]=!0);if(c.x&&c.y&&c.angle)return e;c.x&&c.y||this.trigger("plain",e),c.x||this.trigger("plain:"+n,e),c.y||this.trigger("plain:"+i,e),c.angle||this.trigger("dir dir:"+t,e)}else this.resetDirection();return e};var x=w;function E(e,t){this.nipples=[],this.idles=[],this.actives=[],this.ids=[],this.pressureIntervals={},this.manager=e,this.id=E.id,E.id+=1,this.defaults={zone:document.body,multitouch:!1,maxNumberOfNipples:10,mode:"dynamic",position:{top:0,left:0},catchDistance:200,size:100,threshold:.1,color:"white",fadeTime:250,dataOnly:!1,restJoystick:!0,restOpacity:.5,lockX:!1,lockY:!1,shape:"circle",dynamicPage:!1,follow:!1},this.config(t),"static"!==this.options.mode&&"semi"!==this.options.mode||(this.options.multitouch=!1),this.options.multitouch||(this.options.maxNumberOfNipples=1);e=getComputedStyle(this.options.zone.parentElement);return e&&"flex"===e.display&&(this.parentIsFlex=!0),this.updateBox(),this.prepareNipples(),this.bindings(),this.begin(),this.nipples}E.prototype=new m,(E.constructor=E).id=0,E.prototype.prepareNipples=function(){var i=this.nipples;i.on=this.on.bind(this),i.off=this.off.bind(this),i.options=this.options,i.destroy=this.destroy.bind(this),i.ids=this.ids,i.id=this.id,i.processOnMove=this.processOnMove.bind(this),i.processOnEnd=this.processOnEnd.bind(this),i.get=function(e){if(void 0===e)return i[0];for(var t=0,n=i.length;t