I am trying to do a dual monitor screen recording by combining screen1 and screen2 in Canvas. I am using vue and electron to do this. But I always got a memory leak, after I troubleshoot my code and narrow the problem. I found that this simple code causes a memory leak, but until now I could not find out why drawing inside the canvas cause a memory leak. I also try to clean the canvas before draw but still get a memory leak. My full code is here:
<!--Electron版本選擇共享畫面--> <template> <div v-show="false" class="modal-container"> <section class="modal"> <div class="modalTitleName">ScreenRecord Result</div> <div class="modalTitleBarRightBtn"> <button class="closeButton" v-on:click="close"> <span class="icon" ><img src="images/icon_black_close.svg" style="width: 20px" /></span> </button> </div> <div class="screenshoot-result"> <canvas style="display: none" id="canvasRecord"></canvas> <img id="my-preview" /> <video style="display: none" id="video1" autoplay="autoplay" /> <video style="display: none" id="video2" autoplay="autoplay" /> <!-- <video id="video3" autoplay="autoplay" /> --> </div> <div class="modalSaveButton"> <button class="saveButton"> <span class="icon">Save Screenshoot</span> </button> </div> </section> </div> </template> <script> import { langKey } from "@/scripts/starise-lang.js"; import Logger from "@/scripts/starise-logger"; const logger = Logger("ScreenRecordModule"); export default { name: "ScreenRecordModule", components: {}, props: {}, data: () => { return { langKey: langKey, show: "screen", screenSources: [], imageFormat: "image/jpeg", img: null, width: [], height: [], readyToShow: false, imageSave: null, videoStream: null, mediaRecorder: null, soundRecorder: null, chuncks: [], videoURL: null, streamWrite: null, recorderStream: null, canvas: null, video1: null, video2: null, ctx: null, }; }, created() { // init(window); }, mounted() { logger.debug("mounted"); if (window.electron) { // logger.debug("PATH:%o", window.electron); } this.init(); }, methods: { init() { logger.debug("init"); let _inst = this; this.screenSources = []; if (window.electron) { // 取得現有視窗 window.electron.desktopCapturer .getSources({ types: ["screen"], }) .then(async (sources) => { for (const source of sources) { if (source.id.includes("screen:")) { const stream = await navigator.mediaDevices.getUserMedia({ audio: process.platform === "win32" ? { mandatory: { chromeMediaSource: "desktop", }, } : false, video: { mandatory: { chromeMediaSource: "desktop", chromeMediaSourceId: source.id, // maxWidth: 4000, // maxHeight: 4000, }, }, }); let stream_settings = stream.getVideoTracks()[0].getSettings(); logger.debug("Stream setting:%o", stream_settings); // actual width & height of the camera video let stream_width = stream_settings.width; let stream_height = stream_settings.height; logger.debug("Width: " + stream_width + "px"); logger.debug("Height: " + stream_height + "px"); _inst.screenSources.push(stream); } } try { this.handleStream(_inst.screenSources); } catch (error) { logger.debug("THIS IS SCREENSOURCES ERROR: %o", error); } }); } }, async handleStream(screenSources) { // Create hidden video tag let video = [ document.getElementById("video1"), document.getElementById("video2"), ]; for (let i = 0; i < screenSources.length; i++) { video[i].srcObject = screenSources[i]; video[i].onloadedmetadata = function () { video[i].play(); }; } this.readyToShow = true; logger.debug("Num of Screen: %o", this.screenSources.length); this.video1 = document.getElementById("video1"); this.video2 = document.getElementById("video2"); this.canvas = document.getElementById("canvasRecord"); this.ctx = this.canvas.getContext("2d"); this.canvas.height = 1080; this.canvas.width = 1920 * this.screenSources.length; /* Add audio in and audio in desktop */ const speakerStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false, }); const audioDesktop = await navigator.mediaDevices.getUserMedia({ audio: { mandatory: { chromeMediaSource: "desktop", }, }, video: { mandatory: { chromeMediaSource: "desktop", }, }, }); //Mix the track this.recorderStream = this.mixer(audioDesktop, speakerStream); //Add audio track to canvas stream const canvasStream = this.canvas.captureStream(); canvasStream.addTrack(this.recorderStream.getAudioTracks()[0]); this.mediaRecorder = new MediaRecorder(canvasStream); let chunks = []; this.mediaRecorder.ondataavailable = function (e) { if (e.data.size > 0) { chunks.push(e.data); } }; this.mediaRecorder.onstop = function (e) { let blob = new Blob(chunks, { type: "video/mp4" }); // other types are available such as 'video/webm' for instance, see the doc for more info chunks = []; this.videoURL = URL.createObjectURL(blob); let a = document.createElement("a"); document.body.appendChild(a); a.style = "display: none"; a.href = this.videoURL; a.download = Date.now() + ".mp4"; a.click(); window.URL.revokeObjectURL(this.videoURL); // video3.src = this.videoURL; this.mediaRecorder = null; }; this.mediaRecorder.start(3000); // if (this.screenSources.length > 1) { // window.requestAnimationFrame(this.drawFirstVideo); // window.requestAnimationFrame(this.drawSecondVideo); // } else { // window.requestAnimationFrame(this.drawFirstVideo); // } this.testDraw(); }, testDraw() { // this.ctx.clearRect(0,0,this.canvas.width, this.canvas.height) this.ctx.fillStyle = "#FF0000"; this.ctx.fillRect(0, 0, 150, 75); requestAnimationFrame(this.testDraw); }, drawFirstVideo() { this.ctx.drawImage(this.video1, 0, 0); requestAnimationFrame(this.drawFirstVideo); }, drawSecondVideo() { this.ctx.drawImage(this.video2, 1920, 0); window.requestAnimationFrame(this.drawSecondVideo); }, //Mixing Desktop Audio mixer(windowSource, speakerSource) { const audioContext = new AudioContext(); const mediaStreamDestination = audioContext.createMediaStreamDestination(); if ( windowSource && !!windowSource && windowSource.getAudioTracks().length > 0 ) { logger.debug("windowSource"); audioContext .createMediaStreamSource(windowSource) .connect(mediaStreamDestination); } if ( speakerSource && !!speakerSource && speakerSource.getAudioTracks().length > 0 ) { audioContext .createMediaStreamSource(speakerSource) .connect(mediaStreamDestination); } return new MediaStream( mediaStreamDestination.stream .getTracks() .concat(windowSource.getVideoTracks()) ); }, showContext(type) { this.show = type; }, close() { this.$store.commit("starv/show", { showScreenRecordModule: false }); }, picked(id, type) { window.setDesktopShareSourceId(id, type); this.$store.commit("starv/show", { showScreenshotModule: false }); }, }, watch: { "$store.state.starv.show.startScreenRecord": function (isRecord) { if (!isRecord) { this.mediaRecorder.stop(); this.close(); } }, }, }; </script> <style scoped src="@/styles/ShareScreenPicker.css" />
After narrowing down my problem, I know that this part of my code causes memory leak:
this.testDraw(); }, testDraw() { // this.ctx.clearRect(0,0,this.canvas.width, this.canvas.height) this.ctx.fillStyle = "#FF0000"; this.ctx.fillRect(0, 0, 150, 75); requestAnimationFrame(this.testDraw); },
Has anyone face this same issue before? Thank you
Advertisement
Answer
I found the problem was in this.canvas.captureStream()
. I want to collage 1920×1080 side by side to create dual-monitor screen recorder, therefore I need a big canvas with a width of 3840×1080. I think that Javascript does not have enough time to do a garbage collector, when I tried to do single recording, with the resolution 1920*1080, everything goes to normal.
Here, if we would to do captureStream on the big canvas, we should choose to sacrifice one of two things below:
Set canvas capture stream to less fps for example 15 or 10 fps, for example,
this.captureStream(10)
for 10 fpsSecond option is to rescale to smaller canvas size without sacrifice the fps.