使用 requestAnimationFrame 将视频绘制到 Canvas 时发生内存泄漏
Memory Leak when Drawing Video to Canvas using requestAnimationFrame
我正在尝试通过在 Canvas 中组合 screen1 和 screen2 来进行双显示器屏幕录制。我正在使用 vue 和 electron 来做到这一点。但是在我对代码进行故障排除并缩小问题范围后,我总是会发生内存泄漏。我发现这个简单的代码会导致内存泄漏,但直到现在我都无法找出为什么在 canvas 内绘制会导致内存泄漏。我也尝试在绘制之前清理 canvas 但仍然出现内存泄漏。我的完整代码在这里:
<!--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" />
缩小我的问题后,我知道我的这部分代码导致内存泄漏:
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);
},
有没有人遇到过同样的问题?谢谢
我发现问题出在 this.canvas.captureStream()
。我想并排拼贴 1920x1080 以创建 dual-monitor 屏幕录像机,因此我需要一个宽度为 3840x1080 的大 canvas。我认为Javascript没有足够的时间来做垃圾收集器,当我尝试做单记录时,分辨率为1920*1080,一切正常。
在这里,如果我们要在大 canvas 上执行 captureStream,我们应该选择牺牲以下两件事之一:
将 canvas 捕获流设置为更低的 fps,例如 15 或 10 fps,例如,
this.captureStream(10)
10 帧/秒
第二个选项是在不牺牲 fps 的情况下重新缩放到更小的 canvas 大小。
我正在尝试通过在 Canvas 中组合 screen1 和 screen2 来进行双显示器屏幕录制。我正在使用 vue 和 electron 来做到这一点。但是在我对代码进行故障排除并缩小问题范围后,我总是会发生内存泄漏。我发现这个简单的代码会导致内存泄漏,但直到现在我都无法找出为什么在 canvas 内绘制会导致内存泄漏。我也尝试在绘制之前清理 canvas 但仍然出现内存泄漏。我的完整代码在这里:
<!--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" />
缩小我的问题后,我知道我的这部分代码导致内存泄漏:
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);
},
有没有人遇到过同样的问题?谢谢
我发现问题出在 this.canvas.captureStream()
。我想并排拼贴 1920x1080 以创建 dual-monitor 屏幕录像机,因此我需要一个宽度为 3840x1080 的大 canvas。我认为Javascript没有足够的时间来做垃圾收集器,当我尝试做单记录时,分辨率为1920*1080,一切正常。
在这里,如果我们要在大 canvas 上执行 captureStream,我们应该选择牺牲以下两件事之一:
将 canvas 捕获流设置为更低的 fps,例如 15 或 10 fps,例如,
this.captureStream(10)
10 帧/秒第二个选项是在不牺牲 fps 的情况下重新缩放到更小的 canvas 大小。