一個electron+vue實現錄屏的的demo

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>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章