github地址:https://github.com/zengwangqiu/electron-vue
安装electron最好采用cnpm
main/index.ts
import path from "path";
import window from "./window";
import { dialog, app, ipcMain, globalShortcut, Notification, shell, screen } from "electron";
let mainWindow: Electron.BrowserWindow;
let recorderWindow: Electron.BrowserWindow;
let controlWindow: Electron.BrowserWindow;
let uploadWindow: Electron.BrowserWindow;
let moveWindow: Electron.BrowserWindow;
let saveOnline = false;
let start = false;
let Path = "";
let arean = {
x: 0,
y: 0,
width: 384,
height: 216,
};
let winW = 0;
let winH = 0;
const ffmpegPath = path.join(__dirname, "../..", "ffmpeg.exe");
app.on("ready", () => {
winW = screen.getPrimaryDisplay().workAreaSize.width;
winH = screen.getPrimaryDisplay().workAreaSize.height;
arean.x = (winW - arean.width) / 2;
arean.y = (winH - arean.height) / 2;
// 注册停止快捷键
globalShortcut.register("CommandOrControl+S", () => {
if (start) {
// 通知录制窗口 录制结束
recorderWindow.webContents.send("stop::record", Path, saveOnline, ffmpegPath);
controlWindow.webContents.send("video::creating");
recorderWindow.hide();
moveWindow.hide();
controlWindow.show();
}
});
// 加载一次
// BrowserWindow.addDevToolsExtension(path.resolve(__dirname, "../../devTools/vue-devtools"));
mainWindow = window.create(
path.join(__dirname, "../public/index.html"),
{
width: 500,
height: 300,
},
[
{ name: "setMenu", value: null },
// { name: "setSkipTaskbar", value: true },
],
);
// 主窗口加载完成
mainWindow.webContents.on("did-finish-load", () => {
// 发送根目录
mainWindow.webContents.send("appPath", path.join(__dirname, "../.."));
});
// 打开调试工具
// mainWindow.webContents.openDevTools();
});
ipcMain.on("pick::path", async () => {
const PATH = await dialog.showOpenDialog({ properties: ["openDirectory"] });
Path = PATH.filePaths[0];
mainWindow.webContents.send("path::chosen", Path);
});
// 主窗口点击录制
ipcMain.on("start::record", () => {
if (recorderWindow) { return; }
// 主窗口最小化
mainWindow.minimize();
const recorderURL = path.join(__dirname, "../public/index.html#recorder");
const controlURL = path.join(__dirname, "../public/index.html#control");
const moveURL = path.join(__dirname, "../public/index.html#move");
recorderWindow = window.create(
recorderURL,
{
width: arean.width,
height: arean.height,
x: arean.x,
y: arean.y,
transparent: true,
frame: false,
alwaysOnTop: true,
useContentSize: true,
movable: false,
// modal: true,
// parent: mainWindow,
},
[
{ name: "setMenu", value: null },
],
);
moveWindow = window.create(
moveURL,
{
x: arean.x + arean.width - 32,
y: arean.y + arean.height,
width: 32,
height: 32,
transparent: true,
frame: false,
alwaysOnTop: true,
resizable: false,
useContentSize: true,
parent: recorderWindow,
// modal: true,
},
[
{ name: "setMenu", value: null },
// { name: "setIgnoreMouseEvents", value: true },
// { name: "setFocusable", value: false },
// { name: "setFullScreen", value: true },
],
);
// moveWindow.webContents.openDevTools();
controlWindow = window.create(
controlURL,
{
width: 250,
height: 100,
x: winW / 2 - 125,
y: winH - 100,
alwaysOnTop: true,
resizable: false,
movable: true,
// parent: recorderWindow,
},
[
{ name: "setMenu", value: null },
],
);
// 打开调试工具
// recorderWindow.webContents.openDevTools();
// 改变录制区域大小事件监听
recorderWindow.on("will-resize", (e, newRectangle) => {
if (newRectangle.width < 100 || newRectangle.height < 100) {
e.preventDefault();
} else {
arean = newRectangle;
moveWindow.setPosition(arean.x + arean.width - 32, arean.y + arean.height);
// 通知主窗口更新数据
recorderWindow.webContents.send("arean::size", newRectangle);
}
});
// 改变录制区域大小位置事件监听
moveWindow.on("will-move", (e, newRectangle) => {
if (newRectangle.x + 32 - arean.width < 0 || newRectangle.y - arean.height < 0) {
e.preventDefault();
} else {
arean.x = newRectangle.x + 32 - arean.width;
arean.y = newRectangle.y - arean.height;
recorderWindow.setPosition(arean.x, arean.y);
// 通知主窗口更新数据
recorderWindow.webContents.send("arean::move", arean);
}
});
// 控制窗口关闭
controlWindow.on("closed", () => {
controlWindow = null;
if (recorderWindow) {
recorderWindow.close();
recorderWindow = null;
}
});
recorderWindow.on("closed", () => {
recorderWindow = null;
if (controlWindow) {
controlWindow.close();
controlWindow = null;
}
});
mainWindow.on("closed", () => {
mainWindow = null;
if (recorderWindow) {
recorderWindow.close();
recorderWindow = null;
}
});
});
ipcMain.on("video::finished", (e, message) => {
controlWindow.close();
shell.openItem(Path);
const notification = new Notification({
title: "录制",
body: message,
});
notification.show();
});
// 录制取域选择确认
ipcMain.on("arean::chose", () => {
const sizes = recorderWindow.getContentSize();
const posiontion = recorderWindow.getPosition();
controlWindow.minimize();
recorderWindow.setResizable(false);
// 通知录制窗口开始录制
recorderWindow.webContents.send("arean::chose");
// 通知录制窗口录制区域数据
recorderWindow.webContents.send("arean::size",
{
x: posiontion[0],
y: posiontion[1],
width: sizes[0],
height: sizes[1],
},
);
});
// 录制结束
ipcMain.on("stop::record", () => {
recorderWindow.webContents.send("stop::record", Path, saveOnline, ffmpegPath);
controlWindow.webContents.send("video::creating");
recorderWindow.hide();
moveWindow.hide();
controlWindow.show();
});
// 监听开始上传
ipcMain.on("start::upload", () => {
const URL = __dirname + "../public/index.html#uploading";
uploadWindow = window.create(
URL,
{ width: 680, height: 100 },
[{ name: "setMenu", value: null }],
);
});
// 监听上传进度
ipcMain.on("upload::progress", (e, progress) => {
uploadWindow.webContents.send("upload::progress", progress);
});
// 监听上传完成
ipcMain.on("upload::finish", (e, URL) => {
uploadWindow.webContents.send("upload::finish", URL);
});
// 监听保存
ipcMain.on("choose::save", (e, SaveOnline) => {
saveOnline = SaveOnline;
});
ipcMain.on("recorder::start", () => {
start = true;
});
ipcMain.on("recorder::end", () => {
start = false;
});
main/window.ts
import { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
export type WinMethodsName = "setMenu" | "setIgnoreMouseEvents" |
"setFocusable" | "setFullScreen" | "setSkipTaskbar";
export default {
create(
winUrl: string,
options?: BrowserWindowConstructorOptions,
methods: Array<{ name: WinMethodsName, value: any }> = [],
): BrowserWindow {
let config: BrowserWindowConstructorOptions = {
useContentSize: true,
webPreferences: {
nodeIntegration: true,
nodeIntegrationInWorker: false,
},
};
config = Object.assign(config, options);
let windowId = new BrowserWindow(config);
// tslint:disable-next-line:prefer-for-of
for (let i = 0; i < methods.length; i++) {
windowId[methods[i].name](methods[i].value);
}
windowId.loadURL(winUrl);
windowId.on("closed", () => { windowId = null; });
return windowId;
},
};
router.ts
import Vue from "vue";
import VueRouter, { RouteConfig } from "vue-router";
import Main from "../views/Main.vue";
Vue.use(VueRouter);
const routes: RouteConfig[] = [
{
path: "/",
name: "Main",
component: Main,
},
{
path: "/recorder",
name: "Recorder",
component: () => import(/* webpackChunkName: "recorder" */ "../views/Recorder.vue"),
// component: Recorder,
},
{
path: "/control",
name: "Control",
component: () => import(/* webpackChunkName: "control" */ "../views/Control.vue"),
// component: Control,
},
{
path: "/uploading",
name: "Uploader",
component: () => import(/* webpackChunkName: "transparent" */ "../views/Uploader.vue"),
// component: Uploader,
},
{
path: "/move",
name: "Move",
component: () => import(/* webpackChunkName: "move" */ "../views/Move.vue"),
// component: Move,
},
];
const router = new VueRouter({
mode: "hash",
base: process.env.BASE_URL,
routes,
});
export default router;
Control.vue
<template>
<div class="control">
<div v-if="creating" class="control-tip">视频生成中...div>
<div v-else>
<button
:disabled="creating"
v-if="starting"
class="control-button"
type="button"
@click="stopRecord()"
>停止录制button>
<button
v-else
class="control-button"
type="button"
@click="startRecord()"
:disabled="disabled"
>开始录制button>
div>
div>
template>
<script>
const { ipcRenderer } = window.require("electron");
export default {
name: "Control",
data() {
return {
starting: false,
disabled: false,
creating: false,
};
},
methods: {
startRecord() {
ipcRenderer.send("arean::chose");
this.disabled = true;
this.starting = true;
},
stopRecord() {
ipcRenderer.send("stop::record");
}
},
mounted() {
ipcRenderer.on("video::creating", () => {
this.creating = true;
})
}
};
script>
<style lang="less">
.control {
&-tip {
font-size: 150%;
}
&-text {
font-size: 1.4rem;
margin-bottom: 1rem;
}
&-button {
border-radius: 5px;
border: 1px solid gray;
background-color: lightgray;
height: 30px;
padding: 0 10px;
display: flex;
align-items: center;
}
}
style>
Main.vue
<template>
<div class="control">
<div v-if="creating" class="control-tip">视频生成中...div>
<div v-else>
<button
:disabled="creating"
v-if="starting"
class="control-button"
type="button"
@click="stopRecord()"
>停止录制button>
<button
v-else
class="control-button"
type="button"
@click="startRecord()"
:disabled="disabled"
>开始录制button>
div>
div>
template>
<script>
const { ipcRenderer } = window.require("electron");
export default {
name: "Control",
data() {
return {
starting: false,
disabled: false,
creating: false,
};
},
methods: {
startRecord() {
ipcRenderer.send("arean::chose");
this.disabled = true;
this.starting = true;
},
stopRecord() {
ipcRenderer.send("stop::record");
}
},
mounted() {
ipcRenderer.on("video::creating", () => {
this.creating = true;
})
}
};
script>
<style lang="less">
.control {
&-tip {
font-size: 150%;
}
&-text {
font-size: 1.4rem;
margin-bottom: 1rem;
}
&-button {
border-radius: 5px;
border: 1px solid gray;
background-color: lightgray;
height: 30px;
padding: 0 10px;
display: flex;
align-items: center;
}
}
style>
Move.vue
<template>
<div class="move">
<img src="../../public/move.png" width="32px" height="32px" />
div>
template>
<script>
export default {
name: "Control",
data() {return { };},
methods: {},
mounted() {}
};
script>
<style lang="less">
.move {
cursor: move;
width: 32px;
height: 32px;
-webkit-app-region: drag; //无边框窗口移动的属性
}
style>
style>
Recorder.vue
<template>
<div class="recorder-view" ref="recorder" :style="{borderColor: transparent}">
<div class="mask" v-if="N">
<div v-if="!chose">录制区域div>
<div v-else>
{{N}}秒后开始
<br />Ctrl + S 结束录制
div>
div>
<div style="overflow: hidden;" class="canvas">
<p>预览:p>
<canvas :width="this.arean.width+'px'" :height="this.arean.height+'px'" ref="canvas">canvas>
div>
div>
template>
<script>
const path = window.require("path");
const exec = window.require("child_process").exec;
const fs = window.require("fs");
const { ipcRenderer, desktopCapturer } = window.require("electron");
const borgerWidth = 1;
let recorder;
let blobs = [];
let tracks;
let timer;
const win = window.require('electron').remote.getCurrentWindow();
// import DraggableResizable from "../components/DraggableResizable.vue";
export default {
name: "Recorder",
components: {
// DraggableResizable
},
data() {
return {
arean: { x: 0, y: 0, width: 384, height: 216 },
N: 3,
chose: false,
transparent: ""
}
},
methods: {
start() {
desktopCapturer
.getSources({ types: ["screen"] })
.then(async sources => {
await navigator.webkitGetUserMedia(
{
cursor: "never",
audio: {
mandatory: {
chromeMediaSource: "desktop",
}
},
video: {
mandatory: {
chromeMediaSource: "desktop",
}
}
},
this.handleStream,
err => {
ipcRenderer.send("recorder::faild");
}
);
})
.catch(error => {
ipcRenderer.send("recorder::faild");
});
},
handleStream(screenStream) {
const canvas = this.$refs.canvas;
const ctx = canvas.getContext("2d");
tracks = screenStream.getTracks();
const screenVideoTrack = screenStream.getVideoTracks()[0];
const screenAudioTrack = screenStream.getAudioTracks()[0];
const imageCapture = new ImageCapture(screenVideoTrack);
this.handleImage(ctx, imageCapture);
const options = {
audioBitsPerSecond: 128000,
videoBitsPerSecond: 250000000,
mimeType: "video/webm",
};
const canvasStream = canvas.captureStream(100);
const canvasVideoTrack = canvasStream.getVideoTracks()[0];
const mediaStream = new MediaStream([canvasVideoTrack]);
mediaStream.addTrack(screenAudioTrack);
recorder = new MediaRecorder(mediaStream, options);
blobs = [];
recorder.ondataavailable = function (event) {
blobs.push(event.data);
};
recorder.start();
ipcRenderer.send("recorder::start");
},
handleImage(ctx, imageCapture) {
const self = this;
imageCapture.grabFrame().then(imageBitmap => {
ctx.drawImage(
imageBitmap,
this.arean.x,
this.arean.y,
this.arean.width,
this.arean.height,
0,
0,
this.arean.width,
this.arean.height
);
timer = setTimeout(function () {
self.handleImage(ctx, imageCapture);
}, 0);
});
},
toArrayBuffer(blob, cb) {
const fileReader = new FileReader();
fileReader.onload = function () {
const arrayBuffer = this.result;
cb(arrayBuffer);
};
fileReader.readAsArrayBuffer(blob);
},
toBuffer(ab) {
const buffer = new Buffer(ab.byteLength);
const arr = new Uint8Array(ab);
for (let i = 0; i < arr.byteLength; i++) {
buffer[i] = arr[i];
}
return buffer;
},
stopRecord(userPath, saveOnline, ffmpegPath) {
const self = this;
recorder.onstop = () => {
ipcRenderer.send("recorder::end");
self.toArrayBuffer(new Blob(blobs, { type: "video/webm" }), chunk => {
const buffer = self.toBuffer(chunk);
const randomString = Math.random()
.toString(36)
.substring(7);
const webmName = randomString + "-shot.webm";
const mp4Name = randomString + ".mp4";
const webmPath = path.join(userPath, webmName);
const mp4Path = path.join(userPath, mp4Name);
fs.writeFile(webmPath, buffer, function (err) {
if (!err) {
exec(
`${ffmpegPath} -i ${webmPath} -vcodec h264 ${mp4Path}`,
(error, stdout, stderr) => {
if (error) {
ipcRenderer.send("video::finished", "Failed to save video");
return;
} else {
ipcRenderer.send("video::finished", `saved as:\n ${webmName} \n ${mp4Name}`);
}
}
);
if (saveOnline) {
alert("功能暂未实现!");
ipcRenderer.send("start::upload");
}
} else {
ipcRenderer.send("video::finished", "Failed to save video");
}
});
});
};
clearTimeout(timer);
recorder.stop();
tracks.forEach(track => track.stop());
}
},
mounted() {
const self = this;
const el = self.$refs.recorder;
el.addEventListener('mouseenter', () => {
win.setIgnoreMouseEvents(true, { forward: true })
})
el.addEventListener('mouseleave', () => {
win.setIgnoreMouseEvents(false)
})
ipcRenderer.on("arean::size", (e, arean) => {
arean.x += borgerWidth;
arean.y += borgerWidth;
arean.width -= borgerWidth * 2;
arean.height -= borgerWidth * 2;
self.arean = arean;
});
ipcRenderer.on("arean::move", (e, arean) => {
arean.x += borgerWidth;
arean.y += borgerWidth;
arean.width -= borgerWidth * 2;
arean.height -= borgerWidth * 2;
self.arean = arean;
});
ipcRenderer.on("stop::record", async (e, outputVideoPath, saveOnline, ffmpegPath) => {
self.stopRecord(outputVideoPath, saveOnline, ffmpegPath);
});
ipcRenderer.on("arean::chose", async () => {
self.chose = true;
while (self.N > 0) {
await new Promise(resove => {
setTimeout(() => {
self.N--;
resove();
}, 1000)
})
}
self.start();
});
}
};
script>
<style lang="less">
.recorder-view {
.mask {
display: flex;
align-items: center; /*垂直方向居中*/
justify-content: center;
position: absolute;
background-color: rgba(83, 81, 81, 0.9);
font-size: 500%;
color: aquamarine;
width: 100vw;
height: 100vh;
}
.canvas {
height: 0;
width: 0;
}
display: flex;
align-items: center; /*垂直方向居中*/
justify-content: center; /*水平方向居中*/
width: 100vw;
height: 100vh;
box-sizing: border-box;
border: 1px dashed rgb(250, 123, 123);
}
style>
Uploader.vue
<template>
<div class="uploader">
<p>
Saved to local directory. Now uploading:
<span>{{ progress }}%span>
p>
<p>
Url:
<span>{{ url }}span>
p>
div>
template>
<script>
const { ipcRenderer } = window.require("electron");
export default {
name: "Uploader",
data() {
return {
progress: 0,
url: "",
};
},
mounted() {
ipcRenderer.on("upload::progress", (e, progress) => {
console.log(e, progress);
this.progress = progress;
});
ipcRenderer.on("upload::finish", (e, url) => {
this.url = url;
});
},
};
script>
<style lang="less">
.uploader {
font-size: 1.3rem;
}
style>