├── .gitignore
├── assets
├── logo.ico
├── logo-32x32.ico
└── logo.svg
├── docs
├── assets
│ ├── logo.ico
│ ├── logo-32x32.ico
│ └── logo.svg
├── script.js
├── index.html
└── styles.css
├── .npmrc
├── preload.js
├── package.json
├── README.md
├── index.html
├── main.js
├── LICENSE
└── renderer.js
/.gitignore:
--------------------------------------------------------------------------------
1 | recordings
2 | node_modules
3 | dist
--------------------------------------------------------------------------------
/assets/logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SignitDoc/yi-recorder/HEAD/assets/logo.ico
--------------------------------------------------------------------------------
/docs/assets/logo.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SignitDoc/yi-recorder/HEAD/docs/assets/logo.ico
--------------------------------------------------------------------------------
/assets/logo-32x32.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SignitDoc/yi-recorder/HEAD/assets/logo-32x32.ico
--------------------------------------------------------------------------------
/docs/assets/logo-32x32.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SignitDoc/yi-recorder/HEAD/docs/assets/logo-32x32.ico
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmmirror.com
2 | electron_mirror=https://npmmirror.com/mirrors/electron/
3 | electron-builder-binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
--------------------------------------------------------------------------------
/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/assets/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/preload.js:
--------------------------------------------------------------------------------
1 | const { contextBridge, ipcRenderer } = require("electron");
2 | const electron = require("electron");
3 |
4 | // 设置控制台输出编码
5 | process.env.LANG = "zh_CN.UTF-8";
6 |
7 | // 使用Buffer来确保正确的编码输出
8 | const logWithEncoding = (message) => {
9 | if (typeof message === "string") {
10 | console.log(Buffer.from(message, "utf8").toString());
11 | } else {
12 | console.log(message);
13 | }
14 | };
15 |
16 | logWithEncoding("简化版preload脚本已加载");
17 | logWithEncoding("electron对象: " + typeof electron);
18 | logWithEncoding(
19 | "electron中的desktopCapturer: " + typeof electron.desktopCapturer
20 | );
21 |
22 | contextBridge.exposeInMainWorld("electronAPI", {
23 | captureScreen: async () => {
24 | logWithEncoding("captureScreen函数被调用");
25 | try {
26 | // 通过IPC调用主进程来获取屏幕源
27 | const sources = await ipcRenderer.invoke("get-sources");
28 | logWithEncoding("屏幕源: " + (sources ? sources.length : 0));
29 | return sources;
30 | } catch (error) {
31 | const errorMsg = "捕获屏幕时出错: " + (error.message || error);
32 | logWithEncoding(errorMsg);
33 | throw error;
34 | }
35 | },
36 |
37 | saveFile: (buffer, format) =>
38 | ipcRenderer.send("save-recording", buffer, format),
39 |
40 | onSaveComplete: (callback) => {
41 | ipcRenderer.on("save-recording-response", (_event, response) =>
42 | callback(response)
43 | );
44 | return true;
45 | },
46 |
47 | minimizeWindow: () => ipcRenderer.send("minimize-window"),
48 |
49 | // 显示主窗口
50 | showWindow: () => ipcRenderer.send("show-window"),
51 | });
52 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yi-recorder",
3 | "version": "1.1.0",
4 | "main": "main.js",
5 | "scripts": {
6 | "start": "chcp 65001 && electron .",
7 | "build-win": "electron-builder --win --x64 --publish=never"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "Apache-2.0",
12 | "description": "一个基于Electron的轻量级屏幕录制应用",
13 | "devDependencies": {
14 | "@electron/packager": "^18.3.6",
15 | "electron": "^28.1.0",
16 | "electron-builder": "^24.13.3"
17 | },
18 | "dependencies": {
19 | "ffmpeg-static": "^5.2.0"
20 | },
21 | "build": {
22 | "appId": "cn.signit.yi-recorder",
23 | "productName": "易录屏",
24 | "directories": {
25 | "output": "dist"
26 | },
27 | "win": {
28 | "icon": "assets/logo.ico",
29 | "target": [
30 | {
31 | "target": "nsis",
32 | "arch": [
33 | "x64"
34 | ]
35 | },
36 | {
37 | "target": "portable",
38 | "arch": [
39 | "x64"
40 | ]
41 | }
42 | ],
43 | "artifactName": "${productName}-${version}.${ext}",
44 | "sign": null,
45 | "signingHashAlgorithms": null,
46 | "signDlls": false,
47 | "signAndEditExecutable": false
48 | },
49 | "nsis": {
50 | "oneClick": false,
51 | "allowToChangeInstallationDirectory": true,
52 | "createDesktopShortcut": true,
53 | "createStartMenuShortcut": true,
54 | "shortcutName": "易录屏",
55 | "installerIcon": "assets/logo.ico",
56 | "uninstallerIcon": "assets/logo.ico",
57 | "installerHeaderIcon": "assets/logo.ico",
58 | "artifactName": "${productName}-Setup-${version}.${ext}"
59 | },
60 | "portable": {
61 | "artifactName": "${productName}-Portable-${version}.exe"
62 | },
63 | "asar": true,
64 | "extraResources": [
65 | {
66 | "from": "assets",
67 | "to": "assets"
68 | },
69 | {
70 | "from": "node_modules/ffmpeg-static/ffmpeg.exe",
71 | "to": "ffmpeg/ffmpeg.exe"
72 | }
73 | ]
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 易录屏
2 |
3 | 一个使用 Electron 开发的简洁高效的屏幕录制工具,能够快速录制屏幕活动并保存为视频文件。
4 |
5 | 
6 |
7 | ## 功能特点
8 |
9 | - 🎥 一键开始录制整个屏幕,多屏幕状态下可自由选择要录制屏幕
10 | - ⏱️ 实时显示录制时长
11 | - ⏯️ 支持暂停/恢复录制功能
12 | - 🕒 支持设置最大录制时长,到达时间后自动停止录制
13 | - 👁️ 录制完成后提供视频预览功能
14 | - 💻 录制开始后自动最小化窗口,不影响录制内容
15 | - 💾 支持将录制内容保存为 WebM/MP4 格式
16 | - 🖥️ 跨平台支持(Windows/macOS/Linux)
17 | - 🚀 轻量级,功能精简,启动快速,占用资源少
18 |
19 | ## 截图展示
20 |
21 | 
22 |
23 | ## 安装方法
24 |
25 | ### 从发布版本安装
26 |
27 | 1. 前往 [Releases](https://github.com/yourusername/yi-recorder/releases) 页面
28 | 2. 下载适合您操作系统的安装包:
29 | - **Windows**:
30 | - `易录屏-Setup-1.1.0.exe` - 安装版本
31 | - `易录屏-Portable-1.1.0.exe` - 便携版本
32 | 3. 安装版直接运行安装,便携版可以直接打开使用
33 |
34 | ### 从源码构建
35 |
36 | 1. 克隆仓库
37 |
38 | ```bash
39 | git clone https://github.com/yourusername/yi-recorder.git
40 | cd yi-recorder
41 | ```
42 |
43 | 2. 安装依赖
44 |
45 | ```bash
46 | npm install
47 | ```
48 |
49 | 3. 启动应用(开发模式)
50 |
51 | ```bash
52 | npm start
53 | ```
54 |
55 | 4. 构建应用(生成安装程序和便携版)
56 |
57 | ```bash
58 | npm run build-win
59 | ```
60 |
61 | 构建后的文件会生成在 `dist` 目录下
62 |
63 | ## 使用说明
64 |
65 | 1. 启动应用程序
66 | 2. 可选择是否录制系统声音,并设置最大录制时长(0 表示无限制,默认为 60 分钟)
67 | 3. 点击"开始录制"按钮开始录制屏幕
68 | 4. 多屏幕状态下,选择要录制的屏幕
69 | 5. 应用将在倒计时 3 秒后自动最小化,录制继续在后台进行
70 | 6. 录制过程中,可以点击任务栏中的应用图标,使用"暂停录制"按钮暂停录制,再次点击继续录制
71 | 7. 需要停止录制时,点击任务栏中的应用图标,然后点击"停止录制"
72 | 8. 录制结束后会显示视频预览,可以查看录制内容
73 | 9. 点击"保存录制",选择保存位置即可生成 MP4 格式视频文件
74 |
75 | ## 技术栈
76 |
77 | - Electron - 跨平台桌面应用框架
78 | - JavaScript - 主要编程语言
79 | - HTML/CSS - 用户界面
80 | - WebRTC - 屏幕捕获技术
81 | - FFmpeg - 视频处理
82 |
83 | ## 开发计划
84 |
85 | - [x] 支持录制视频另存为 mp4 格式
86 | - [x] 支持多屏幕选择
87 | - [x] 支持音频录制
88 | - [x] 添加暂停/恢复录制功能
89 | - [x] 添加录制预览功能
90 | - [x] 添加录制时长限制功能
91 | - [ ] 添加录制区域选择功能
92 | - [ ] 支持对录制后的视频进行二次处理
93 |
94 | ## 贡献指南
95 |
96 | 欢迎贡献代码、报告问题或提出新功能建议。请遵循以下步骤:
97 |
98 | 1. Fork 本仓库
99 | 2. 创建您的特性分支 (`git checkout -b feature/amazing-feature`)
100 | 3. 提交您的更改 (`git commit -m 'Add some amazing feature'`)
101 | 4. 推送到分支 (`git push origin feature/amazing-feature`)
102 | 5. 开启一个 Pull Request
103 |
104 | ## 开源许可
105 |
106 | 本项目采用 [Apache License 2.0](LICENSE) 许可证。
107 |
108 | ## 版权声明
109 |
110 | Copyright 2025 易录屏
111 |
112 | ---
113 |
114 | 如果您觉得这个项目有用,请给它一个星标 ⭐️!
115 |
--------------------------------------------------------------------------------
/docs/script.js:
--------------------------------------------------------------------------------
1 | // 平滑滚动
2 | document.querySelectorAll('a[href^="#"]').forEach(anchor => {
3 | anchor.addEventListener('click', function (e) {
4 | e.preventDefault();
5 |
6 | const targetId = this.getAttribute('href');
7 | const targetElement = document.querySelector(targetId);
8 |
9 | if (targetElement) {
10 | window.scrollTo({
11 | top: targetElement.offsetTop - 80, // 考虑导航栏高度
12 | behavior: 'smooth'
13 | });
14 | }
15 | });
16 | });
17 |
18 | // 导航栏滚动效果
19 | window.addEventListener('scroll', function() {
20 | const header = document.querySelector('header');
21 | if (window.scrollY > 50) {
22 | header.style.boxShadow = '0 4px 10px rgba(0, 0, 0, 0.1)';
23 | header.style.background = 'rgba(255, 255, 255, 0.95)';
24 | } else {
25 | header.style.boxShadow = '0 2px 10px rgba(0, 0, 0, 0.1)';
26 | header.style.background = 'var(--white)';
27 | }
28 | });
29 |
30 | // 动画效果
31 | document.addEventListener('DOMContentLoaded', function() {
32 | // 检测元素是否在视口中
33 | function isInViewport(element) {
34 | const rect = element.getBoundingClientRect();
35 | return (
36 | rect.top <= (window.innerHeight || document.documentElement.clientHeight) &&
37 | rect.bottom >= 0
38 | );
39 | }
40 |
41 | // 添加动画类
42 | function addAnimationClass() {
43 | const elements = document.querySelectorAll('.feature-card, .step, .download-card');
44 |
45 | elements.forEach(element => {
46 | if (isInViewport(element) && !element.classList.contains('animated')) {
47 | element.classList.add('animated');
48 | element.style.opacity = '1';
49 | element.style.transform = element.classList.contains('feature-card')
50 | ? 'translateY(0)'
51 | : 'translateX(0)';
52 | }
53 | });
54 | }
55 |
56 | // 初始化元素样式
57 | const elements = document.querySelectorAll('.feature-card, .step, .download-card');
58 | elements.forEach(element => {
59 | element.style.opacity = '0';
60 | element.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
61 |
62 | if (element.classList.contains('feature-card')) {
63 | element.style.transform = 'translateY(20px)';
64 | } else {
65 | element.style.transform = 'translateX(-20px)';
66 | }
67 | });
68 |
69 | // 初始检查
70 | addAnimationClass();
71 |
72 | // 滚动时检查
73 | window.addEventListener('scroll', addAnimationClass);
74 | });
75 |
76 | // 版本号更新
77 | document.addEventListener('DOMContentLoaded', function() {
78 | // 获取当前年份
79 | const currentYear = new Date().getFullYear();
80 |
81 | // 更新版权信息中的年份
82 | const copyrightYear = document.querySelector('.copyright p');
83 | if (copyrightYear) {
84 | copyrightYear.innerHTML = copyrightYear.innerHTML.replace(/\d{4}/, currentYear);
85 | }
86 | });
87 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 易录屏
7 |
296 |
297 |
298 |
299 |
300 |
301 |
302 |
请选择要录制的屏幕
303 |
304 |
305 |
306 |
307 |
308 |
309 |
310 |
311 |
312 |
易录屏
313 |
314 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
00:00:00
334 |
335 |
336 | 准备就绪。点击"开始录制"按钮开始全屏录制。
337 |
338 |
339 |
340 |
341 |
录制预览
342 |
343 |
344 |
345 |
346 |
347 |
350 |
351 |
352 |
353 |
354 |
--------------------------------------------------------------------------------
/main.js:
--------------------------------------------------------------------------------
1 | const {
2 | app,
3 | BrowserWindow,
4 | ipcMain,
5 | Menu,
6 | dialog,
7 | desktopCapturer,
8 | } = require("electron");
9 | const path = require("path");
10 | const fs = require("fs");
11 | const { exec } = require("child_process");
12 | const os = require("os");
13 | const ffmpegPath = require("ffmpeg-static");
14 |
15 | // 设置控制台输出编码
16 | process.env.LANG = "zh_CN.UTF-8";
17 |
18 | // 确保子进程使用UTF-8编码
19 | process.env.PYTHONIOENCODING = "UTF-8";
20 |
21 | // 创建一个帮助函数来确保正确的编码输出
22 | const logWithEncoding = (message) => {
23 | if (typeof message === "string") {
24 | console.log(Buffer.from(message, "utf8").toString());
25 | } else {
26 | console.log(message);
27 | }
28 | };
29 |
30 | // 重写错误日志函数
31 | const errorWithEncoding = (message, error) => {
32 | if (typeof message === "string") {
33 | console.error(Buffer.from(message, "utf8").toString(), error);
34 | } else {
35 | console.error(message, error);
36 | }
37 | };
38 |
39 | // 获取FFmpeg可执行文件路径
40 | function getFfmpegPath() {
41 | // 修复打包后的ffmpeg路径问题
42 | // 判断是开发环境还是生产环境
43 | if (app.isPackaged) {
44 | // 打包后的环境,通过process.resourcesPath找到资源目录
45 | const ffmpegName = process.platform === "win32" ? "ffmpeg.exe" : "ffmpeg";
46 | // 检查resources目录是否存在ffmpeg
47 | const resourcesPath = path.join(
48 | process.resourcesPath,
49 | "ffmpeg",
50 | ffmpegName
51 | );
52 | if (fs.existsSync(resourcesPath)) {
53 | return resourcesPath;
54 | }
55 | // 如果resources目录不存在,尝试在应用目录下查找
56 | const appPath = path.join(
57 | path.dirname(app.getAppPath()),
58 | "ffmpeg",
59 | ffmpegName
60 | );
61 | if (fs.existsSync(appPath)) {
62 | return appPath;
63 | }
64 | logWithEncoding("使用默认ffmpeg路径: " + ffmpegPath);
65 | }
66 | return ffmpegPath;
67 | }
68 |
69 | // 隐藏菜单栏
70 | Menu.setApplicationMenu(null);
71 |
72 | let mainWindow;
73 |
74 | function createWindow() {
75 | // 检查preload脚本是否存在
76 | const preloadPath = path.join(__dirname, "preload.js");
77 | logWithEncoding("Preload脚本路径: " + preloadPath);
78 | logWithEncoding("Preload脚本存在: " + fs.existsSync(preloadPath));
79 |
80 | mainWindow = new BrowserWindow({
81 | width: 600,
82 | height: 380,
83 | resizable: false,
84 | maximizable: false,
85 | fullscreenable: false,
86 | webPreferences: {
87 | preload: preloadPath,
88 | contextIsolation: true,
89 | nodeIntegration: false,
90 | sandbox: false, // 禁用沙盒以允许更多功能
91 | // 允许访问媒体设备和捕获系统音频
92 | audioCapturerEnabled: true,
93 | },
94 | icon: path.join(__dirname, "assets/logo.ico"),
95 | });
96 |
97 | mainWindow.loadFile("index.html");
98 |
99 | // 开发时打开开发者工具
100 | // mainWindow.webContents.openDevTools();
101 |
102 | mainWindow.on("closed", () => {
103 | mainWindow = null;
104 | });
105 | }
106 |
107 | // 添加应用启动参数,允许录制系统音频
108 | app.commandLine.appendSwitch("enable-features", "WebRTCAudioCapturing");
109 | app.commandLine.appendSwitch("enable-usermedia-screen-capturing");
110 |
111 | app.whenReady().then(createWindow);
112 |
113 | app.on("window-all-closed", () => {
114 | if (process.platform !== "darwin") {
115 | app.quit();
116 | }
117 | });
118 |
119 | app.on("activate", () => {
120 | if (mainWindow === null) {
121 | createWindow();
122 | }
123 | });
124 |
125 | // 处理获取屏幕源
126 | ipcMain.handle("get-sources", async () => {
127 | try {
128 | const { screen } = require("electron");
129 | const displays = screen.getAllDisplays();
130 | const sources = await desktopCapturer.getSources({
131 | types: ["screen"],
132 | thumbnailSize: { width: 100, height: 100 },
133 | });
134 |
135 | sources.forEach((source, index) => {
136 | source.displaySize = `${displays[index].size.width}x${displays[index].size.height}`;
137 | });
138 |
139 | return sources;
140 | } catch (error) {
141 | errorWithEncoding("获取屏幕源出错:", error);
142 | throw error;
143 | }
144 | });
145 |
146 | // 处理保存录制文件
147 | ipcMain.on("save-recording", async (event, buffer, format = "mp4") => {
148 | // 根据选择的格式设置对话框选项
149 | const isWebm = format === "webm";
150 | const fileExtension = isWebm ? "webm" : "mp4";
151 | const fileTypeLabel = isWebm ? "WebM 文件" : "MP4 文件";
152 |
153 | const { filePath } = await dialog.showSaveDialog({
154 | buttonLabel: "保存视频",
155 | defaultPath: `recording-${Date.now()}.${fileExtension}`,
156 | filters: [{ name: fileTypeLabel, extensions: [fileExtension] }],
157 | });
158 |
159 | if (filePath) {
160 | // 创建临时WebM文件
161 | const tempDir = os.tmpdir();
162 | const tempWebmPath = path.join(tempDir, `temp-${Date.now()}.webm`);
163 |
164 | logWithEncoding("临时WebM路径: " + tempWebmPath);
165 | logWithEncoding("最终目标路径: " + filePath);
166 | logWithEncoding("选择的格式: " + format);
167 |
168 | // 先保存WebM文件
169 | fs.writeFile(tempWebmPath, buffer, async (err) => {
170 | if (err) {
171 | errorWithEncoding("保存临时WebM文件失败:", err);
172 | event.reply("save-recording-response", {
173 | success: false,
174 | message: "保存失败:" + err.message,
175 | });
176 | return;
177 | }
178 |
179 | try {
180 | // 如果选择WebM格式,直接重命名文件
181 | if (isWebm) {
182 | fs.renameSync(tempWebmPath, filePath);
183 |
184 | event.reply("save-recording-response", {
185 | success: true,
186 | message: "保存成功",
187 | filePath,
188 | });
189 | return;
190 | }
191 |
192 | // 如果选择MP4格式,需要转换
193 | // 创建临时MP4文件路径
194 | const tempMp4Path = path.join(tempDir, `temp-${Date.now()}.mp4`);
195 | logWithEncoding("临时MP4路径: " + tempMp4Path);
196 |
197 | // 获取并验证FFmpeg路径
198 | const ffmpegPath = getFfmpegPath();
199 | logWithEncoding("使用的FFmpeg路径: " + ffmpegPath);
200 |
201 | if (!fs.existsSync(ffmpegPath)) {
202 | throw new Error("找不到FFmpeg可执行文件: " + ffmpegPath);
203 | }
204 |
205 | // 使用FFmpeg转换WebM为MP4
206 | await new Promise((resolve, reject) => {
207 | const command = `"${ffmpegPath}" -i "${tempWebmPath}" -c:v libx264 -preset medium -crf 23 "${tempMp4Path}"`;
208 | logWithEncoding("执行的FFmpeg命令: " + command);
209 |
210 | // 设置子进程的编码为UTF-8
211 | exec(
212 | command,
213 | {
214 | encoding: "utf8",
215 | env: {
216 | ...process.env,
217 | PYTHONIOENCODING: "UTF-8",
218 | LANG: "zh_CN.UTF-8",
219 | },
220 | },
221 | (error, stdout, stderr) => {
222 | if (error) {
223 | errorWithEncoding("FFmpeg错误:", error);
224 | errorWithEncoding("FFmpeg输出:", stderr);
225 | reject(new Error(`FFmpeg转换失败: ${error.message}`));
226 | return;
227 | }
228 | resolve();
229 | }
230 | );
231 | });
232 |
233 | // 将转换后的MP4文件移动到目标位置
234 | fs.renameSync(tempMp4Path, filePath);
235 |
236 | // 清理临时文件
237 | fs.unlinkSync(tempWebmPath);
238 |
239 | event.reply("save-recording-response", {
240 | success: true,
241 | message: "保存成功",
242 | filePath,
243 | });
244 | } catch (error) {
245 | errorWithEncoding("处理视频文件失败:", error);
246 | // 清理临时文件
247 | try {
248 | if (fs.existsSync(tempWebmPath)) {
249 | fs.unlinkSync(tempWebmPath);
250 | }
251 | if (
252 | format === "mp4" &&
253 | fs.existsSync(path.join(tempDir, `temp-${Date.now()}.mp4`))
254 | ) {
255 | fs.unlinkSync(path.join(tempDir, `temp-${Date.now()}.mp4`));
256 | }
257 | } catch (cleanupError) {
258 | errorWithEncoding("清理临时文件失败:", cleanupError);
259 | }
260 |
261 | // 提供更详细的错误信息
262 | let errorMessage = "处理视频文件失败";
263 | if (error.message) {
264 | errorMessage += ": " + error.message;
265 | }
266 | if (error.code) {
267 | errorMessage += " (错误代码: " + error.code + ")";
268 | }
269 |
270 | event.reply("save-recording-response", {
271 | success: false,
272 | message: errorMessage,
273 | });
274 | }
275 | });
276 | } else {
277 | event.reply("save-recording-response", {
278 | success: false,
279 | message: "保存取消",
280 | });
281 | }
282 | });
283 |
284 | // 处理最小化窗口请求
285 | ipcMain.on("minimize-window", () => {
286 | if (mainWindow) {
287 | mainWindow.minimize();
288 | }
289 | });
290 |
291 | // 处理显示窗口请求
292 | ipcMain.on("show-window", () => {
293 | if (mainWindow) {
294 | mainWindow.show();
295 | mainWindow.focus();
296 | }
297 | });
298 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | 易录屏 - 简洁高效的屏幕录制工具
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |

18 |
易录屏
19 |
20 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
易录屏
39 |
一个使用 Electron 开发的简洁高效的屏幕录制工具
40 |
41 | 快速录制屏幕活动并保存为视频文件,轻量级,功能精简,启动快速,占用资源少
42 |
43 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
功能特点
55 |
56 |
57 |
58 |
一键录制
59 |
一键开始录制整个屏幕,多屏幕状态下可自由选择要录制屏幕
60 |
61 |
62 |
63 |
实时计时
64 |
实时显示录制时长,掌握录制进度
65 |
66 |
67 |
68 |
暂停/恢复
69 |
支持暂停/恢复录制功能,灵活控制录制过程
70 |
71 |
72 |
73 |
时长限制
74 |
支持设置最大录制时长,到达时间后自动停止录制
75 |
76 |
77 |
78 |
视频预览
79 |
录制完成后提供视频预览功能,即时查看录制效果
80 |
81 |
82 |
83 |
84 |
85 |
自动最小化
86 |
录制开始后自动最小化窗口,不影响录制内容
87 |
88 |
89 |
90 |
多格式保存
91 |
支持将录制内容保存为 WebM/MP4 格式
92 |
93 |
94 |
95 |
跨平台支持
96 |
支持 Windows/macOS/Linux 多平台使用
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
截图展示
105 |
106 |

107 |
108 |
109 |
110 |
111 |
112 |
113 |
下载安装
114 |
115 |
116 |
117 |
Windows
118 |
124 |
125 |
126 |
127 |
macOS
128 |
即将推出
129 |
130 |
131 |
132 |
Linux
133 |
即将推出
134 |
135 |
136 |
137 |
从源码构建
138 |
139 |
git clone https://github.com/SignitDoc/yi-recorder.git
140 | cd yi-recorder
141 | npm install
142 | npm start
143 |
144 |
构建应用(生成安装程序和便携版):
145 |
146 |
npm run build-win
147 |
148 |
构建后的文件会生成在 dist 目录下
149 |
150 |
151 |
152 |
153 |
154 |
155 |
使用说明
156 |
157 |
158 |
1
159 |
160 |
启动应用程序
161 |
双击安装后的应用图标启动易录屏
162 |
163 |
164 |
165 |
2
166 |
167 |
设置录制选项
168 |
169 | 可选择是否录制系统声音,并设置最大录制时长(0 表示无限制,默认为
170 | 60 分钟)
171 |
172 |
173 |
174 |
175 |
3
176 |
177 |
开始录制
178 |
179 | 点击"开始录制"按钮开始录制屏幕,多屏幕状态下,选择要录制的屏幕
180 |
181 |
182 |
183 |
184 |
4
185 |
186 |
录制过程
187 |
应用将在倒计时 3 秒后自动最小化,录制继续在后台进行
188 |
189 |
190 |
191 |
5
192 |
193 |
暂停/继续
194 |
195 | 录制过程中,可以点击任务栏中的应用图标,使用"暂停录制"按钮暂停录制,再次点击继续录制
196 |
197 |
198 |
199 |
200 |
6
201 |
202 |
停止录制
203 |
需要停止录制时,点击任务栏中的应用图标,然后点击"停止录制"
204 |
205 |
206 |
207 |
7
208 |
209 |
预览和保存
210 |
211 | 录制结束后会显示视频预览,可以查看录制内容,点击"保存录制",选择保存位置即可生成视频文件
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
260 |
261 |
262 |
263 |
264 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | Copyright 2024 易录屏
179 |
180 | Licensed under the Apache License, Version 2.0 (the "License");
181 | you may not use this file except in compliance with the License.
182 | You may obtain a copy of the License at
183 |
184 | http://www.apache.org/licenses/LICENSE-2.0
185 |
186 | Unless required by applicable law or agreed to in writing, software
187 | distributed under the License is distributed on an "AS IS" BASIS,
188 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
189 | See the License for the specific language governing permissions and
190 | limitations under the License.
191 |
--------------------------------------------------------------------------------
/docs/styles.css:
--------------------------------------------------------------------------------
1 | /* 全局样式 */
2 | :root {
3 | --primary-color: #4a86e8;
4 | --secondary-color: #34a853;
5 | --accent-color: #ea4335;
6 | --dark-color: #333333;
7 | --light-color: #f8f9fa;
8 | --gray-color: #6c757d;
9 | --light-gray: #e9ecef;
10 | --white: #ffffff;
11 | --shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
12 | --transition: all 0.3s ease;
13 | }
14 |
15 | * {
16 | margin: 0;
17 | padding: 0;
18 | box-sizing: border-box;
19 | }
20 |
21 | body {
22 | font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
23 | line-height: 1.6;
24 | color: var(--dark-color);
25 | background-color: var(--light-color);
26 | }
27 |
28 | .container {
29 | width: 100%;
30 | max-width: 1200px;
31 | margin: 0 auto;
32 | padding: 0 20px;
33 | }
34 |
35 | a {
36 | text-decoration: none;
37 | color: var(--primary-color);
38 | transition: var(--transition);
39 | }
40 |
41 | a:hover {
42 | color: var(--secondary-color);
43 | }
44 |
45 | h1, h2, h3, h4, h5, h6 {
46 | margin-bottom: 1rem;
47 | line-height: 1.2;
48 | }
49 |
50 | h2 {
51 | font-size: 2.5rem;
52 | text-align: center;
53 | margin-bottom: 2rem;
54 | position: relative;
55 | padding-bottom: 0.5rem;
56 | }
57 |
58 | h2::after {
59 | content: '';
60 | position: absolute;
61 | bottom: 0;
62 | left: 50%;
63 | transform: translateX(-50%);
64 | width: 80px;
65 | height: 4px;
66 | background-color: var(--primary-color);
67 | border-radius: 2px;
68 | }
69 |
70 | section {
71 | padding: 80px 0;
72 | }
73 |
74 | section:nth-child(even) {
75 | background-color: var(--white);
76 | }
77 |
78 | .btn {
79 | display: inline-block;
80 | padding: 12px 24px;
81 | border-radius: 50px;
82 | font-weight: 600;
83 | text-align: center;
84 | cursor: pointer;
85 | transition: var(--transition);
86 | border: none;
87 | }
88 |
89 | .btn-primary {
90 | background-color: var(--primary-color);
91 | color: var(--white);
92 | }
93 |
94 | .btn-primary:hover {
95 | background-color: #3a76d8;
96 | color: var(--white);
97 | transform: translateY(-2px);
98 | box-shadow: var(--shadow);
99 | }
100 |
101 | .btn-secondary {
102 | background-color: var(--white);
103 | color: var(--primary-color);
104 | border: 2px solid var(--primary-color);
105 | }
106 |
107 | .btn-secondary:hover {
108 | background-color: var(--primary-color);
109 | color: var(--white);
110 | transform: translateY(-2px);
111 | box-shadow: var(--shadow);
112 | }
113 |
114 | .btn-download {
115 | background-color: var(--secondary-color);
116 | color: var(--white);
117 | margin: 5px;
118 | }
119 |
120 | .btn-download:hover {
121 | background-color: #2d9748;
122 | color: var(--white);
123 | transform: translateY(-2px);
124 | box-shadow: var(--shadow);
125 | }
126 |
127 | /* 导航栏 */
128 | header {
129 | background-color: var(--white);
130 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
131 | position: sticky;
132 | top: 0;
133 | z-index: 1000;
134 | }
135 |
136 | header .container {
137 | display: flex;
138 | justify-content: space-between;
139 | align-items: center;
140 | padding: 15px 20px;
141 | }
142 |
143 | .logo {
144 | display: flex;
145 | align-items: center;
146 | }
147 |
148 | .logo img {
149 | height: 40px;
150 | margin-right: 10px;
151 | }
152 |
153 | .logo h1 {
154 | font-size: 1.5rem;
155 | margin-bottom: 0;
156 | color: var(--primary-color);
157 | }
158 |
159 | nav ul {
160 | display: flex;
161 | list-style: none;
162 | }
163 |
164 | nav ul li {
165 | margin-left: 20px;
166 | }
167 |
168 | nav ul li a {
169 | color: var(--dark-color);
170 | font-weight: 500;
171 | padding: 5px 10px;
172 | border-radius: 4px;
173 | }
174 |
175 | nav ul li a:hover {
176 | color: var(--primary-color);
177 | }
178 |
179 | .github-link {
180 | display: flex;
181 | align-items: center;
182 | }
183 |
184 | .github-link i {
185 | margin-right: 5px;
186 | }
187 |
188 | /* 英雄区 */
189 | .hero {
190 | padding: 100px 0;
191 | background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
192 | }
193 |
194 | .hero .container {
195 | display: flex;
196 | align-items: center;
197 | justify-content: space-between;
198 | }
199 |
200 | .hero-content {
201 | flex: 1;
202 | padding-right: 40px;
203 | }
204 |
205 | .hero-content h1 {
206 | font-size: 3.5rem;
207 | margin-bottom: 1rem;
208 | color: var(--primary-color);
209 | }
210 |
211 | .hero-content .subtitle {
212 | font-size: 1.5rem;
213 | margin-bottom: 1rem;
214 | color: var(--dark-color);
215 | }
216 |
217 | .hero-content .description {
218 | font-size: 1.1rem;
219 | margin-bottom: 2rem;
220 | color: var(--gray-color);
221 | }
222 |
223 | .cta-buttons {
224 | display: flex;
225 | gap: 15px;
226 | }
227 |
228 | .hero-image {
229 | flex: 1;
230 | display: flex;
231 | justify-content: center;
232 | align-items: center;
233 | }
234 |
235 | .hero-image img {
236 | max-width: 100%;
237 | height: auto;
238 | max-height: 400px;
239 | border-radius: 10px;
240 | box-shadow: var(--shadow);
241 | }
242 |
243 | /* 功能特点 */
244 | .features-grid {
245 | display: grid;
246 | grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
247 | gap: 30px;
248 | margin-top: 40px;
249 | }
250 |
251 | .feature-card {
252 | background-color: var(--white);
253 | border-radius: 10px;
254 | padding: 30px;
255 | box-shadow: var(--shadow);
256 | transition: var(--transition);
257 | text-align: center;
258 | }
259 |
260 | .feature-card:hover {
261 | transform: translateY(-10px);
262 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
263 | }
264 |
265 | .feature-icon {
266 | font-size: 2.5rem;
267 | color: var(--primary-color);
268 | margin-bottom: 20px;
269 | }
270 |
271 | .feature-card h3 {
272 | font-size: 1.5rem;
273 | margin-bottom: 15px;
274 | }
275 |
276 | /* 截图展示 */
277 | .screenshot-container {
278 | margin-top: 40px;
279 | text-align: center;
280 | }
281 |
282 | .screenshot-container img {
283 | max-width: 100%;
284 | border-radius: 10px;
285 | box-shadow: var(--shadow);
286 | }
287 |
288 | /* 下载安装 */
289 | .download-options {
290 | display: flex;
291 | justify-content: center;
292 | flex-wrap: wrap;
293 | gap: 30px;
294 | margin-bottom: 50px;
295 | }
296 |
297 | .download-card {
298 | background-color: var(--white);
299 | border-radius: 10px;
300 | padding: 30px;
301 | box-shadow: var(--shadow);
302 | text-align: center;
303 | width: 300px;
304 | transition: var(--transition);
305 | }
306 |
307 | .download-card:hover {
308 | transform: translateY(-5px);
309 | box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
310 | }
311 |
312 | .download-icon {
313 | font-size: 3rem;
314 | color: var(--primary-color);
315 | margin-bottom: 20px;
316 | }
317 |
318 | .download-card h3 {
319 | font-size: 1.5rem;
320 | margin-bottom: 20px;
321 | }
322 |
323 | .download-links {
324 | display: flex;
325 | flex-direction: column;
326 | gap: 10px;
327 | }
328 |
329 | .coming-soon {
330 | opacity: 0.7;
331 | }
332 |
333 | .coming-soon p {
334 | font-style: italic;
335 | color: var(--gray-color);
336 | }
337 |
338 | .source-build {
339 | background-color: var(--white);
340 | border-radius: 10px;
341 | padding: 30px;
342 | box-shadow: var(--shadow);
343 | }
344 |
345 | .code-block {
346 | background-color: var(--dark-color);
347 | border-radius: 5px;
348 | padding: 15px;
349 | margin: 15px 0;
350 | overflow-x: auto;
351 | }
352 |
353 | .code-block pre {
354 | margin: 0;
355 | }
356 |
357 | .code-block code {
358 | color: var(--light-color);
359 | font-family: 'Consolas', 'Courier New', monospace;
360 | font-size: 0.9rem;
361 | }
362 |
363 | /* 使用说明 */
364 | .usage-steps {
365 | margin-top: 40px;
366 | }
367 |
368 | .step {
369 | display: flex;
370 | margin-bottom: 30px;
371 | background-color: var(--white);
372 | border-radius: 10px;
373 | padding: 20px;
374 | box-shadow: var(--shadow);
375 | }
376 |
377 | .step-number {
378 | display: flex;
379 | justify-content: center;
380 | align-items: center;
381 | width: 50px;
382 | height: 50px;
383 | background-color: var(--primary-color);
384 | color: var(--white);
385 | border-radius: 50%;
386 | font-size: 1.5rem;
387 | font-weight: bold;
388 | margin-right: 20px;
389 | flex-shrink: 0;
390 | }
391 |
392 | .step-content h3 {
393 | margin-bottom: 10px;
394 | }
395 |
396 | /* 页脚 */
397 | footer {
398 | background-color: var(--dark-color);
399 | color: var(--light-color);
400 | padding: 60px 0 20px;
401 | }
402 |
403 | .footer-content {
404 | display: flex;
405 | flex-wrap: wrap;
406 | justify-content: space-between;
407 | margin-bottom: 40px;
408 | }
409 |
410 | .footer-logo {
411 | display: flex;
412 | align-items: center;
413 | margin-bottom: 20px;
414 | }
415 |
416 | .footer-logo img {
417 | height: 40px;
418 | margin-right: 10px;
419 | }
420 |
421 | .footer-logo h3 {
422 | color: var(--white);
423 | margin-bottom: 0;
424 | }
425 |
426 | .footer-links, .footer-tech {
427 | margin-bottom: 20px;
428 | }
429 |
430 | .footer-links h4, .footer-tech h4 {
431 | color: var(--white);
432 | margin-bottom: 15px;
433 | }
434 |
435 | .footer-links ul, .footer-tech ul {
436 | list-style: none;
437 | }
438 |
439 | .footer-links ul li, .footer-tech ul li {
440 | margin-bottom: 10px;
441 | }
442 |
443 | .footer-links ul li a {
444 | color: var(--light-gray);
445 | }
446 |
447 | .footer-links ul li a:hover {
448 | color: var(--white);
449 | }
450 |
451 | .footer-tech ul li {
452 | color: var(--light-gray);
453 | }
454 |
455 | .copyright {
456 | text-align: center;
457 | padding-top: 20px;
458 | border-top: 1px solid rgba(255, 255, 255, 0.1);
459 | color: var(--light-gray);
460 | }
461 |
462 | .copyright a {
463 | color: var(--light-gray);
464 | }
465 |
466 | .copyright a:hover {
467 | color: var(--white);
468 | }
469 |
470 | /* 响应式设计 */
471 | @media (max-width: 992px) {
472 | .hero .container {
473 | flex-direction: column;
474 | text-align: center;
475 | }
476 |
477 | .hero-content {
478 | padding-right: 0;
479 | margin-bottom: 40px;
480 | }
481 |
482 | .cta-buttons {
483 | justify-content: center;
484 | }
485 | }
486 |
487 | @media (max-width: 768px) {
488 | header .container {
489 | flex-direction: column;
490 | }
491 |
492 | nav ul {
493 | margin-top: 15px;
494 | flex-wrap: wrap;
495 | justify-content: center;
496 | }
497 |
498 | nav ul li {
499 | margin: 5px 10px;
500 | }
501 |
502 | .footer-content {
503 | flex-direction: column;
504 | }
505 | }
506 |
507 | @media (max-width: 576px) {
508 | .hero-content h1 {
509 | font-size: 2.5rem;
510 | }
511 |
512 | .hero-content .subtitle {
513 | font-size: 1.2rem;
514 | }
515 |
516 | .cta-buttons {
517 | flex-direction: column;
518 | gap: 10px;
519 | }
520 |
521 | .step {
522 | flex-direction: column;
523 | align-items: center;
524 | text-align: center;
525 | }
526 |
527 | .step-number {
528 | margin-right: 0;
529 | margin-bottom: 15px;
530 | }
531 | }
532 |
--------------------------------------------------------------------------------
/renderer.js:
--------------------------------------------------------------------------------
1 | // DOM元素
2 | const recordBtn = document.getElementById("recordBtn");
3 | const pauseBtn = document.getElementById("pauseBtn");
4 | const stopBtn = document.getElementById("stopBtn");
5 | const saveBtn = document.getElementById("saveBtn");
6 | const timerElement = document.getElementById("timer");
7 | const statusElement = document.getElementById("status");
8 | const recordAudioCheckbox = document.getElementById("recordAudio");
9 | const maxDurationInput = document.getElementById("maxDuration");
10 | const loadingOverlay = document.getElementById("loadingOverlay");
11 | const videoPreviewContainer = document.getElementById("videoPreviewContainer");
12 | const videoPreview = document.getElementById("videoPreview");
13 | let selectedSource = null;
14 |
15 | // 录制状态变量
16 | let mediaRecorder;
17 | let recordedChunks = [];
18 | let stream;
19 | let startTime;
20 | let pausedTime = 0;
21 | let totalPausedTime = 0;
22 | let lastPauseTime = 0;
23 | let isPaused = false;
24 | let timerInterval;
25 | let maxDurationMs = 0; // 最大录制时长(毫秒)
26 | let maxDurationTimer = null; // 最大录制时长定时器
27 |
28 | // 初始化
29 | document.addEventListener("DOMContentLoaded", () => {
30 | // 检查API是否可用
31 | console.log("API可用性检查:", !!window.electronAPI);
32 | if (window.electronAPI) {
33 | console.log("可用的API:", Object.keys(window.electronAPI));
34 | }
35 |
36 | // 初始化按钮状态
37 | recordBtn.style.display = "inline-block"; // 显示开始录制按钮
38 | pauseBtn.style.display = "none"; // 隐藏暂停录制按钮
39 | stopBtn.style.display = "none"; // 隐藏停止录制按钮
40 | saveBtn.style.display = "none"; // 隐藏保存录制按钮
41 |
42 | recordBtn.addEventListener("click", startRecording);
43 | pauseBtn.addEventListener("click", togglePauseRecording);
44 | stopBtn.addEventListener("click", stopRecording);
45 | saveBtn.addEventListener("click", saveRecording);
46 |
47 | // 设置保存响应处理
48 | window.electronAPI.onSaveComplete(handleSaveResponse);
49 | });
50 |
51 | // 启动录制
52 | async function startRecording() {
53 | try {
54 | // 隐藏视频预览
55 | videoPreviewContainer.style.display = "none";
56 |
57 | // 获取最大录制时长设置(分钟)
58 | const maxDurationMinutes = parseInt(maxDurationInput.value, 10) || 0;
59 | maxDurationMs = maxDurationMinutes * 60 * 1000; // 转换为毫秒
60 |
61 | // 清除之前的最大时长定时器(如果有)
62 | if (maxDurationTimer) {
63 | clearTimeout(maxDurationTimer);
64 | maxDurationTimer = null;
65 | }
66 |
67 | statusElement.textContent = "正在获取屏幕源...";
68 |
69 | // 获取可用的屏幕源
70 | const sources = await window.electronAPI.captureScreen();
71 | if (!sources || sources.length === 0) {
72 | throw new Error("找不到可用的屏幕源");
73 | }
74 |
75 | // 只有一个屏幕时自动选择,多个屏幕时显示前端对话框
76 | recordBtn.style.display = "none"; // 隐藏开始录制按钮
77 | if (sources.length === 1) {
78 | statusElement.textContent = "检测到单个屏幕,自动选择...";
79 | selectedSource = sources[0];
80 | } else {
81 | statusElement.textContent = "请选择要录制的屏幕";
82 | selectedSource = await showScreenSelectionDialog(sources);
83 | if (!selectedSource) {
84 | statusElement.textContent = "已取消屏幕选择";
85 | recordBtn.style.display = "inline-block"; // 重新显示开始录制按钮
86 | return;
87 | }
88 | }
89 |
90 | statusElement.textContent = "准备开始录制...";
91 | await showCountdown(3);
92 |
93 | // 设置媒体约束
94 | const constraints = {
95 | audio: recordAudioCheckbox.checked
96 | ? {
97 | mandatory: {
98 | chromeMediaSource: "desktop",
99 | },
100 | }
101 | : false,
102 | video: {
103 | mandatory: {
104 | chromeMediaSource: "desktop",
105 | chromeMediaSourceId: selectedSource.id,
106 | },
107 | },
108 | };
109 |
110 | console.log("使用的媒体约束:", JSON.stringify(constraints));
111 |
112 | // 获取媒体流
113 | stream = await navigator.mediaDevices.getUserMedia(constraints);
114 |
115 | // 创建MediaRecorder实例
116 | mediaRecorder = new MediaRecorder(stream, {
117 | mimeType: "video/webm; codecs=vp9",
118 | });
119 |
120 | // 收集录制的数据
121 | mediaRecorder.ondataavailable = (e) => {
122 | if (e.data.size > 0) {
123 | recordedChunks.push(e.data);
124 | }
125 | };
126 |
127 | // 录制结束处理
128 | mediaRecorder.onstop = () => {
129 | stopTimer();
130 | statusElement.textContent = `录制已完成。${
131 | recordAudioCheckbox.checked ? "包含系统声音。" : ""
132 | }可以保存录制内容。`;
133 | };
134 |
135 | // 开始录制
136 | mediaRecorder.start(100);
137 | startTimer();
138 |
139 | // 设置最大录制时长定时器(如果设置了最大时长)
140 | if (maxDurationMs > 0) {
141 | maxDurationTimer = setTimeout(() => {
142 | if (mediaRecorder && mediaRecorder.state !== "inactive") {
143 | // 显示应用窗口
144 | window.electronAPI.showWindow();
145 |
146 | // 停止录制
147 | stopRecording();
148 |
149 | // 提示用户已达到最大录制时长
150 | statusElement.textContent = `已达到最大录制时长 ${maxDurationMinutes} 分钟,录制已自动停止。`;
151 | }
152 | }, maxDurationMs);
153 | }
154 |
155 | // 更新UI状态
156 | recordBtn.style.display = "none"; // 隐藏开始录制按钮
157 | pauseBtn.style.display = "inline-block"; // 显示暂停录制按钮
158 | stopBtn.style.display = "inline-block"; // 显示停止录制按钮
159 | saveBtn.style.display = "none"; // 隐藏保存录制按钮
160 | recordAudioCheckbox.disabled = true;
161 | maxDurationInput.disabled = true; // 录制中禁用最大时长设置
162 | recordBtn.classList.add("recording");
163 | recordBtn.textContent = "正在录制";
164 | pauseBtn.textContent = "暂停录制";
165 | pauseBtn.classList.remove("paused");
166 | isPaused = false;
167 | totalPausedTime = 0;
168 |
169 | // 更新状态信息,包括最大录制时长
170 | let statusText = `正在录制屏幕${
171 | recordAudioCheckbox.checked ? "和系统声音" : ""
172 | }`;
173 | if (maxDurationMs > 0) {
174 | statusText += `,最大录制时长: ${maxDurationMinutes} 分钟`;
175 | }
176 | statusElement.textContent = statusText + "...";
177 |
178 | // 最小化窗口并开始录制
179 | window.electronAPI.minimizeWindow();
180 | } catch (error) {
181 | console.error("启动录制时出错:", error);
182 | statusElement.textContent = `录制失败: ${error.message}`;
183 | recordBtn.style.display = "inline-block"; // 重新显示开始录制按钮
184 | pauseBtn.style.display = "none"; // 隐藏暂停录制按钮
185 | stopBtn.style.display = "none"; // 隐藏停止录制按钮
186 | saveBtn.style.display = "none"; // 隐藏保存录制按钮
187 | recordAudioCheckbox.disabled = false;
188 | maxDurationInput.disabled = false;
189 | }
190 | }
191 |
192 | // 暂停/继续录制
193 | function togglePauseRecording() {
194 | if (!mediaRecorder || mediaRecorder.state === "inactive") {
195 | return;
196 | }
197 |
198 | if (isPaused) {
199 | // 继续录制
200 | resumeRecording();
201 | } else {
202 | // 暂停录制
203 | pauseRecording();
204 | }
205 | }
206 |
207 | // 暂停录制
208 | function pauseRecording() {
209 | if (!mediaRecorder || mediaRecorder.state !== "recording") {
210 | return;
211 | }
212 |
213 | // 暂停 MediaRecorder
214 | mediaRecorder.pause();
215 |
216 | // 记录暂停时间
217 | lastPauseTime = Date.now();
218 |
219 | // 暂停计时器
220 | stopTimer();
221 |
222 | // 如果设置了最大录制时长,暂停定时器
223 | if (maxDurationTimer) {
224 | clearTimeout(maxDurationTimer);
225 | // 计算剩余时间
226 | const elapsedTime = Date.now() - startTime - totalPausedTime;
227 | const remainingTime = Math.max(0, maxDurationMs - elapsedTime);
228 | // 保存剩余时间
229 | maxDurationMs = remainingTime;
230 | }
231 |
232 | // 更新UI状态
233 | pauseBtn.textContent = "继续录制";
234 | pauseBtn.classList.add("paused");
235 | statusElement.textContent = "录制已暂停";
236 | isPaused = true;
237 | }
238 |
239 | // 继续录制
240 | function resumeRecording() {
241 | if (!mediaRecorder || mediaRecorder.state !== "paused") {
242 | return;
243 | }
244 |
245 | // 继续 MediaRecorder
246 | mediaRecorder.resume();
247 |
248 | // 计算暂停时间
249 | const currentTime = Date.now();
250 | const pauseDuration = currentTime - lastPauseTime;
251 | totalPausedTime += pauseDuration;
252 |
253 | // 继续计时器
254 | startTimer();
255 |
256 | // 如果设置了最大录制时长,重新设置定时器
257 | if (maxDurationMs > 0) {
258 | maxDurationTimer = setTimeout(() => {
259 | if (mediaRecorder && mediaRecorder.state !== "inactive") {
260 | // 显示应用窗口
261 | window.electronAPI.showWindow();
262 |
263 | // 停止录制
264 | stopRecording();
265 |
266 | // 提示用户已达到最大录制时长
267 | const maxDurationMinutes = Math.ceil(maxDurationMs / (60 * 1000));
268 | statusElement.textContent = `已达到最大录制时长 ${maxDurationMinutes} 分钟,录制已自动停止。`;
269 | }
270 | }, maxDurationMs);
271 | }
272 |
273 | // 更新UI状态
274 | pauseBtn.textContent = "暂停录制";
275 | pauseBtn.classList.remove("paused");
276 |
277 | // 更新状态信息,包括最大录制时长
278 | let statusText = `继续录制屏幕${
279 | recordAudioCheckbox.checked ? "和系统声音" : ""
280 | }`;
281 | if (maxDurationMs > 0) {
282 | const maxDurationMinutes = Math.ceil(maxDurationMs / (60 * 1000));
283 | statusText += `,剩余时间: 约 ${maxDurationMinutes} 分钟`;
284 | }
285 | statusElement.textContent = statusText + "...";
286 |
287 | isPaused = false;
288 | }
289 |
290 | // 停止录制
291 | function stopRecording() {
292 | if (!mediaRecorder || mediaRecorder.state === "inactive") {
293 | return;
294 | }
295 |
296 | // 清除最大录制时长定时器
297 | if (maxDurationTimer) {
298 | clearTimeout(maxDurationTimer);
299 | maxDurationTimer = null;
300 | }
301 |
302 | mediaRecorder.stop();
303 | stream.getTracks().forEach((track) => track.stop());
304 |
305 | // 更新UI状态
306 | recordBtn.style.display = "inline-block"; // 显示开始录制按钮
307 | pauseBtn.style.display = "none"; // 隐藏暂停录制按钮
308 | stopBtn.style.display = "none"; // 隐藏停止录制按钮
309 | saveBtn.style.display = "inline-block"; // 显示保存录制按钮
310 | recordAudioCheckbox.disabled = false;
311 | maxDurationInput.disabled = false; // 重新启用最大时长设置
312 | recordBtn.classList.remove("recording");
313 | pauseBtn.classList.remove("paused");
314 | recordBtn.textContent = "开始录制";
315 | pauseBtn.textContent = "暂停录制";
316 | isPaused = false;
317 |
318 | // 创建并显示视频预览
319 | createVideoPreview();
320 | }
321 |
322 | // 创建视频预览
323 | function createVideoPreview() {
324 | if (!recordedChunks.length) {
325 | return;
326 | }
327 |
328 | // 创建视频Blob
329 | const blob = new Blob(recordedChunks, { type: "video/webm" });
330 |
331 | // 创建视频URL
332 | const videoURL = URL.createObjectURL(blob);
333 |
334 | // 设置视频源
335 | videoPreview.src = videoURL;
336 |
337 | // 显示视频预览容器
338 | videoPreviewContainer.style.display = "block";
339 |
340 | // 视频加载完成后自动播放
341 | videoPreview.onloadedmetadata = () => {
342 | videoPreview.play();
343 | };
344 | }
345 |
346 | // 保存录制
347 | function saveRecording() {
348 | if (!recordedChunks.length) {
349 | statusElement.textContent = "没有录制内容可保存";
350 | return;
351 | }
352 |
353 | // 显示格式选择弹窗
354 | showFormatSelectionDialog();
355 | }
356 |
357 | // 显示格式选择对话框
358 | function showFormatSelectionDialog() {
359 | // 创建弹窗元素
360 | const modal = document.createElement("div");
361 | modal.className = "modal";
362 | modal.style.display = "flex";
363 | modal.style.zIndex = "3000";
364 |
365 | const modalContent = document.createElement("div");
366 | modalContent.className = "modal-content";
367 | modalContent.style.maxWidth = "400px";
368 |
369 | const title = document.createElement("div");
370 | title.className = "modal-title";
371 | title.textContent = "请选择保存格式";
372 | title.style.marginBottom = "20px";
373 |
374 | const buttonsContainer = document.createElement("div");
375 | buttonsContainer.style.display = "flex";
376 | buttonsContainer.style.justifyContent = "space-around";
377 | buttonsContainer.style.marginTop = "20px";
378 |
379 | // WebM按钮
380 | const webmButton = document.createElement("button");
381 | webmButton.textContent = "保存为WebM格式";
382 | webmButton.style.backgroundColor = "#3498db";
383 | webmButton.style.marginRight = "10px";
384 |
385 | // MP4按钮
386 | const mp4Button = document.createElement("button");
387 | mp4Button.textContent = "保存为MP4格式";
388 | mp4Button.style.backgroundColor = "#2ecc71";
389 |
390 | // 添加按钮点击事件
391 | webmButton.addEventListener("click", () => {
392 | modal.style.display = "none";
393 | document.body.removeChild(modal);
394 | processAndSaveRecording("webm");
395 | });
396 |
397 | mp4Button.addEventListener("click", () => {
398 | modal.style.display = "none";
399 | document.body.removeChild(modal);
400 | processAndSaveRecording("mp4");
401 | });
402 |
403 | // 组装弹窗
404 | buttonsContainer.appendChild(webmButton);
405 | buttonsContainer.appendChild(mp4Button);
406 | modalContent.appendChild(title);
407 | modalContent.appendChild(buttonsContainer);
408 | modal.appendChild(modalContent);
409 |
410 | // 添加到页面
411 | document.body.appendChild(modal);
412 | }
413 |
414 | // 处理并保存录制内容
415 | function processAndSaveRecording(format) {
416 | statusElement.textContent = "正在处理录制内容,请稍候...";
417 | loadingOverlay.style.display = "flex";
418 |
419 | // 隐藏视频预览
420 | videoPreviewContainer.style.display = "none";
421 |
422 | // 合并所有录制的片段
423 | const blob = new Blob(recordedChunks, { type: "video/webm" });
424 |
425 | // 将Blob转换为Buffer
426 | const reader = new FileReader();
427 | reader.onload = () => {
428 | const buffer = new Uint8Array(reader.result);
429 |
430 | // 通过IPC发送到主进程保存,并指定格式
431 | window.electronAPI.saveFile(buffer, format);
432 | statusElement.textContent = `正在保存为${format.toUpperCase()}格式...`;
433 | };
434 |
435 | reader.readAsArrayBuffer(blob);
436 | }
437 |
438 | // 处理保存录制文件的响应
439 | function handleSaveResponse(response) {
440 | loadingOverlay.style.display = "none";
441 |
442 | if (response.success) {
443 | statusElement.textContent = `${response.message}:${response.filePath}`;
444 | // 清除录制的数据
445 | recordedChunks = [];
446 | saveBtn.style.display = "none"; // 隐藏保存录制按钮
447 |
448 | // 清除视频预览
449 | videoPreview.src = "";
450 | videoPreviewContainer.style.display = "none";
451 |
452 | // 释放视频URL资源
453 | if (videoPreview.src) {
454 | URL.revokeObjectURL(videoPreview.src);
455 | }
456 | } else {
457 | statusElement.textContent = response.message;
458 | }
459 | }
460 |
461 | // 计时器功能
462 | function startTimer() {
463 | if (!isPaused) {
464 | // 如果是第一次开始录制
465 | startTime = Date.now();
466 | }
467 | updateTimer();
468 | timerInterval = setInterval(updateTimer, 1000);
469 | }
470 |
471 | function stopTimer() {
472 | clearInterval(timerInterval);
473 | }
474 |
475 | function updateTimer() {
476 | // 计算实际录制时间,减去暂停的时间
477 | const currentTime = Date.now();
478 | const elapsedTime = currentTime - startTime - totalPausedTime;
479 |
480 | const seconds = Math.floor((elapsedTime / 1000) % 60);
481 | const minutes = Math.floor((elapsedTime / (1000 * 60)) % 60);
482 | const hours = Math.floor(elapsedTime / (1000 * 60 * 60));
483 |
484 | timerElement.textContent = `${padZero(hours)}:${padZero(minutes)}:${padZero(
485 | seconds
486 | )}`;
487 | }
488 |
489 | function padZero(num) {
490 | return num.toString().padStart(2, "0");
491 | }
492 |
493 | // 显示屏幕选择对话框
494 | function showScreenSelectionDialog(sources) {
495 | return new Promise((resolve) => {
496 | const modal = document.getElementById("screenModal");
497 | const screenList = document.getElementById("screenList");
498 | const confirmBtn = document.getElementById("confirmScreenSelect");
499 | const cancelBtn = document.getElementById("cancelScreenSelect");
500 |
501 | // 清空并重新填充屏幕列表
502 | screenList.innerHTML = "";
503 | let selectedSource = null;
504 |
505 | sources.forEach((source) => {
506 | const item = document.createElement("div");
507 | item.className = "screen-item";
508 | item.innerHTML = `
509 |
510 |
${source.displaySize}
511 |
512 | ${source.name}
513 | `;
514 |
515 | item.addEventListener("click", () => {
516 | // 更新选中状态
517 | document.querySelectorAll(".screen-item").forEach((el) => {
518 | el.classList.remove("selected");
519 | });
520 | item.classList.add("selected");
521 | selectedSource = source;
522 | confirmBtn.style.display = "inline-block"; // 显示确认按钮
523 | });
524 |
525 | screenList.appendChild(item);
526 | });
527 |
528 | // 确认按钮点击处理
529 | confirmBtn.addEventListener(
530 | "click",
531 | () => {
532 | modal.style.display = "none";
533 | resolve(selectedSource);
534 | },
535 | { once: true }
536 | );
537 |
538 | // 取消按钮点击处理
539 | cancelBtn.addEventListener(
540 | "click",
541 | () => {
542 | modal.style.display = "none";
543 | resolve(null);
544 | },
545 | { once: true }
546 | );
547 |
548 | // 显示对话框
549 | modal.style.display = "flex";
550 | confirmBtn.style.display = "none"; // 初始隐藏确认按钮,直到选择了屏幕
551 | });
552 | }
553 |
554 | // 显示倒计时动画
555 | async function showCountdown(seconds) {
556 | return new Promise((resolve) => {
557 | const countdownOverlay = document.createElement("div");
558 | countdownOverlay.style.position = "fixed";
559 | countdownOverlay.style.top = "0";
560 | countdownOverlay.style.left = "0";
561 | countdownOverlay.style.width = "100%";
562 | countdownOverlay.style.height = "100%";
563 | countdownOverlay.style.backgroundColor = "rgba(0,0,0,0.7)";
564 | countdownOverlay.style.display = "flex";
565 | countdownOverlay.style.justifyContent = "center";
566 | countdownOverlay.style.alignItems = "center";
567 | countdownOverlay.style.zIndex = "2000";
568 | countdownOverlay.style.fontSize = "120px";
569 | countdownOverlay.style.color = "white";
570 | countdownOverlay.style.fontWeight = "bold";
571 | countdownOverlay.style.textShadow = "0 0 20px #3498db";
572 |
573 | document.body.appendChild(countdownOverlay);
574 |
575 | let count = seconds;
576 | countdownOverlay.textContent = count;
577 |
578 | const timer = setInterval(() => {
579 | count--;
580 | if (count <= 0) {
581 | clearInterval(timer);
582 | document.body.removeChild(countdownOverlay);
583 | resolve();
584 | } else {
585 | countdownOverlay.textContent = count;
586 | }
587 | }, 1000);
588 | });
589 | }
590 |
--------------------------------------------------------------------------------