eedatek-electron-screenrecord 2.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/demo.d.ts +1 -0
- package/lib/demo.js +84 -0
- package/lib/event.d.ts +4 -0
- package/lib/event.js +9 -0
- package/lib/getDisplay.d.ts +7 -0
- package/lib/getDisplay.js +16 -0
- package/lib/index.cjs.d.ts +0 -0
- package/lib/index.cjs.js +2 -0
- package/lib/index.d.ts +68 -0
- package/lib/index.js +429 -0
- package/lib/padStart.d.ts +8 -0
- package/lib/padStart.js +17 -0
- package/lib/preload.d.ts +11 -0
- package/lib/preload.js +63 -0
- package/lib/worker.d.ts +10 -0
- package/lib/worker.js +95 -0
- package/package.json +57 -0
package/lib/demo.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/lib/demo.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/* eslint-disable no-console */
|
|
7
|
+
const electron_1 = require("electron");
|
|
8
|
+
const _1 = __importDefault(require("."));
|
|
9
|
+
const padStart_1 = __importDefault(require("./padStart"));
|
|
10
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
|
+
const console_1 = require("console");
|
|
12
|
+
let mediaStream; // 视频流
|
|
13
|
+
let audioStream; // 声音流
|
|
14
|
+
let mediaRecorder; // 媒体录制器对象
|
|
15
|
+
let recordedChunks; // 存储录制的音频数据
|
|
16
|
+
let sreenSources; //所有屏幕
|
|
17
|
+
let worker;
|
|
18
|
+
electron_1.app.whenReady().then(() => {
|
|
19
|
+
const screenshots = new _1.default({ singleWindow: false, logger: console_1.log });
|
|
20
|
+
electron_1.globalShortcut.register('ctrl+shift+a', () => {
|
|
21
|
+
screenshots.startCapture();
|
|
22
|
+
});
|
|
23
|
+
screenshots.on('windowCreated', ($win) => {
|
|
24
|
+
$win.on('focus', () => {
|
|
25
|
+
electron_1.globalShortcut.register('esc', () => {
|
|
26
|
+
screenshots.endCapture();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
//$win.webContents.openDevTools();
|
|
30
|
+
// const defaultSession = session.defaultSession;
|
|
31
|
+
// defaultSession.setDisplayMediaRequestHandler((request, callback) => {
|
|
32
|
+
// desktopCapturer.getSources({types: ['screen']}).then((sources) => {
|
|
33
|
+
// // Grant access to the first screen found.
|
|
34
|
+
// callback({video: sources[0]});
|
|
35
|
+
// });
|
|
36
|
+
// });
|
|
37
|
+
// defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
|
|
38
|
+
// if (permission === 'media') {
|
|
39
|
+
// console.log('33333333333')
|
|
40
|
+
// return callback(true)
|
|
41
|
+
// }
|
|
42
|
+
// return callback(true)
|
|
43
|
+
// })
|
|
44
|
+
$win.on('blur', () => {
|
|
45
|
+
electron_1.globalShortcut.unregister('esc');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
// 防止不能关闭
|
|
49
|
+
electron_1.globalShortcut.register('ctrl+shift+q', () => {
|
|
50
|
+
electron_1.app.quit();
|
|
51
|
+
});
|
|
52
|
+
// 录屏结束
|
|
53
|
+
screenshots.on('stop', async (e, data) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
screenshots.endCapture();
|
|
56
|
+
if (data) {
|
|
57
|
+
const time = new Date();
|
|
58
|
+
const year = time.getFullYear();
|
|
59
|
+
const month = (0, padStart_1.default)(time.getMonth() + 1, 2, '0');
|
|
60
|
+
const date = (0, padStart_1.default)(time.getDate(), 2, '0');
|
|
61
|
+
const hours = (0, padStart_1.default)(time.getHours(), 2, '0');
|
|
62
|
+
const minutes = (0, padStart_1.default)(time.getMinutes(), 2, '0');
|
|
63
|
+
const seconds = (0, padStart_1.default)(time.getSeconds(), 2, '0');
|
|
64
|
+
const milliseconds = (0, padStart_1.default)(time.getMilliseconds(), 3, '0');
|
|
65
|
+
const { canceled, filePath } = await electron_1.dialog.showSaveDialog(mainWin, {
|
|
66
|
+
defaultPath: `${year}${month}${date}${hours}${minutes}${seconds}${milliseconds}.webm`,
|
|
67
|
+
});
|
|
68
|
+
if (canceled || !filePath) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await fs_extra_1.default.writeFile(filePath, Buffer.from(data));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const mainWin = new electron_1.BrowserWindow({
|
|
75
|
+
show: true,
|
|
76
|
+
});
|
|
77
|
+
mainWin.removeMenu();
|
|
78
|
+
mainWin.loadURL('https://www.j2l3x.com/');
|
|
79
|
+
});
|
|
80
|
+
electron_1.app.on('window-all-closed', () => {
|
|
81
|
+
if (process.platform !== 'darwin') {
|
|
82
|
+
electron_1.app.quit();
|
|
83
|
+
}
|
|
84
|
+
});
|
package/lib/event.d.ts
ADDED
package/lib/event.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const electron_1 = require("electron");
|
|
4
|
+
exports.default = () => {
|
|
5
|
+
const point = electron_1.screen.getCursorScreenPoint();
|
|
6
|
+
const { id, bounds, scaleFactor } = electron_1.screen.getDisplayNearestPoint(point);
|
|
7
|
+
// https://github.com/nashaofu/screenshots/issues/98
|
|
8
|
+
return {
|
|
9
|
+
id,
|
|
10
|
+
x: Math.floor(bounds.x),
|
|
11
|
+
y: Math.floor(bounds.y),
|
|
12
|
+
width: Math.floor(bounds.width),
|
|
13
|
+
height: Math.floor(bounds.height),
|
|
14
|
+
scaleFactor,
|
|
15
|
+
};
|
|
16
|
+
};
|
|
File without changes
|
package/lib/index.cjs.js
ADDED
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { Debugger } from "debug";
|
|
3
|
+
import { BrowserWindow } from "electron";
|
|
4
|
+
import Events from "events";
|
|
5
|
+
import { Bounds } from "./preload";
|
|
6
|
+
export type LoggerFn = (...args: unknown[]) => void;
|
|
7
|
+
export type Logger = Debugger | LoggerFn;
|
|
8
|
+
export interface Lang {
|
|
9
|
+
magnifier_position_label?: string;
|
|
10
|
+
operation_ok_title?: string;
|
|
11
|
+
operation_cancel_title?: string;
|
|
12
|
+
operation_save_title?: string;
|
|
13
|
+
operation_redo_title?: string;
|
|
14
|
+
operation_undo_title?: string;
|
|
15
|
+
operation_mosaic_title?: string;
|
|
16
|
+
operation_text_title?: string;
|
|
17
|
+
operation_brush_title?: string;
|
|
18
|
+
operation_arrow_title?: string;
|
|
19
|
+
operation_ellipse_title?: string;
|
|
20
|
+
operation_rectangle_title?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ScreenRecordOpts {
|
|
23
|
+
lang?: Lang;
|
|
24
|
+
logger?: Logger;
|
|
25
|
+
singleWindow?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export { Bounds };
|
|
28
|
+
export default class ScreenRecord extends Events {
|
|
29
|
+
$win: BrowserWindow | null;
|
|
30
|
+
private logger;
|
|
31
|
+
private singleWindow;
|
|
32
|
+
private isReadyState;
|
|
33
|
+
private buttonBounds;
|
|
34
|
+
private isRecording;
|
|
35
|
+
private mouseMoveListener;
|
|
36
|
+
constructor(opts?: ScreenRecordOpts);
|
|
37
|
+
updateButtonBounds(bounds: {
|
|
38
|
+
x: number;
|
|
39
|
+
y: number;
|
|
40
|
+
width: number;
|
|
41
|
+
height: number;
|
|
42
|
+
}): void;
|
|
43
|
+
private startMouseTracking;
|
|
44
|
+
private stopMouseTracking;
|
|
45
|
+
private isMouseInButtonArea;
|
|
46
|
+
/**
|
|
47
|
+
* 开始录屏
|
|
48
|
+
*/
|
|
49
|
+
startCapture(): Promise<void>;
|
|
50
|
+
/**
|
|
51
|
+
* 结束录屏
|
|
52
|
+
*/
|
|
53
|
+
endCapture(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* 设置语言
|
|
56
|
+
*/
|
|
57
|
+
setLang(lang: Partial<Lang>): Promise<void>;
|
|
58
|
+
private reset;
|
|
59
|
+
/**
|
|
60
|
+
* 初始化窗口
|
|
61
|
+
*/
|
|
62
|
+
private createWindow;
|
|
63
|
+
private capture;
|
|
64
|
+
/**
|
|
65
|
+
* 绑定ipc时间处理
|
|
66
|
+
*/
|
|
67
|
+
private listenIpc;
|
|
68
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const debug_1 = __importDefault(require("debug"));
|
|
7
|
+
const electron_1 = require("electron");
|
|
8
|
+
const events_1 = __importDefault(require("events"));
|
|
9
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
10
|
+
const event_1 = __importDefault(require("./event"));
|
|
11
|
+
const getDisplay_1 = __importDefault(require("./getDisplay"));
|
|
12
|
+
const padStart_1 = __importDefault(require("./padStart"));
|
|
13
|
+
const path_1 = __importDefault(require("path"));
|
|
14
|
+
class ScreenRecord extends events_1.default {
|
|
15
|
+
// 截图窗口对象
|
|
16
|
+
$win = null;
|
|
17
|
+
logger;
|
|
18
|
+
singleWindow;
|
|
19
|
+
isReadyState = false;
|
|
20
|
+
buttonBounds;
|
|
21
|
+
isRecording = false;
|
|
22
|
+
mouseMoveListener = null;
|
|
23
|
+
constructor(opts) {
|
|
24
|
+
super();
|
|
25
|
+
this.logger = opts?.logger || (0, debug_1.default)("electron-screenshots");
|
|
26
|
+
this.singleWindow = opts?.singleWindow || false;
|
|
27
|
+
this.listenIpc();
|
|
28
|
+
this.buttonBounds = { x: 0, y: 0, width: 100, height: 100 };
|
|
29
|
+
}
|
|
30
|
+
//更新开始、结束录屏按钮区域
|
|
31
|
+
updateButtonBounds(bounds) {
|
|
32
|
+
this.logger(`updateButtonBounds x:${bounds.x} y:${bounds.y} w:${bounds.width} h:${bounds.height}`);
|
|
33
|
+
this.buttonBounds = bounds;
|
|
34
|
+
}
|
|
35
|
+
startMouseTracking() {
|
|
36
|
+
if (this.mouseMoveListener)
|
|
37
|
+
return;
|
|
38
|
+
// 监听全局鼠标移动
|
|
39
|
+
this.mouseMoveListener = (event) => {
|
|
40
|
+
if (!this.isRecording)
|
|
41
|
+
return;
|
|
42
|
+
const mousePos = electron_1.screen.getCursorScreenPoint();
|
|
43
|
+
const winBounds = this.$win?.getBounds();
|
|
44
|
+
// 计算鼠标是否在按钮区域内
|
|
45
|
+
const isInButtonArea = this.isMouseInButtonArea(mousePos, winBounds);
|
|
46
|
+
if (isInButtonArea) {
|
|
47
|
+
// 鼠标在按钮区域,启用鼠标事件
|
|
48
|
+
this.$win?.setIgnoreMouseEvents(false);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
// 鼠标不在按钮区域,忽略鼠标事件
|
|
52
|
+
this.$win?.setIgnoreMouseEvents(true, { forward: true });
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
// 高频监听鼠标移动
|
|
56
|
+
setInterval(this.mouseMoveListener, 50); // 每50ms检查一次
|
|
57
|
+
}
|
|
58
|
+
stopMouseTracking() {
|
|
59
|
+
if (this.mouseMoveListener) {
|
|
60
|
+
clearInterval(this.mouseMoveListener);
|
|
61
|
+
this.mouseMoveListener = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
isMouseInButtonArea(mousePos, winBounds) {
|
|
65
|
+
// 计算按钮在屏幕上的绝对位置
|
|
66
|
+
const buttonScreenPos = {
|
|
67
|
+
x: winBounds.x + this.buttonBounds.x,
|
|
68
|
+
y: winBounds.y + this.buttonBounds.y,
|
|
69
|
+
width: this.buttonBounds.width,
|
|
70
|
+
height: this.buttonBounds.height,
|
|
71
|
+
};
|
|
72
|
+
return (mousePos.x >= buttonScreenPos.x &&
|
|
73
|
+
mousePos.x <= buttonScreenPos.x + buttonScreenPos.width &&
|
|
74
|
+
mousePos.y >= buttonScreenPos.y &&
|
|
75
|
+
mousePos.y <= buttonScreenPos.y + buttonScreenPos.height);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* 开始录屏
|
|
79
|
+
*/
|
|
80
|
+
async startCapture() {
|
|
81
|
+
this.logger("startCapture");
|
|
82
|
+
const display = (0, getDisplay_1.default)();
|
|
83
|
+
//全屏的截图base64
|
|
84
|
+
const [imageUrl] = await Promise.all([this.capture(display)]);
|
|
85
|
+
await this.createWindow(display, imageUrl);
|
|
86
|
+
}
|
|
87
|
+
// /**
|
|
88
|
+
// * 发送提示消息
|
|
89
|
+
// */
|
|
90
|
+
// public async sendTip(message: string): Promise<void> {
|
|
91
|
+
// this.logger('sendtip');
|
|
92
|
+
// this.$view.webContents.send('SCREENRECORD:tip', message);
|
|
93
|
+
// }
|
|
94
|
+
/**
|
|
95
|
+
* 结束录屏
|
|
96
|
+
*/
|
|
97
|
+
async endCapture() {
|
|
98
|
+
this.logger("endCapture");
|
|
99
|
+
await this.reset();
|
|
100
|
+
if (!this.$win) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
// 先清除 Kiosk 模式,然后取消全屏才有效
|
|
104
|
+
this.$win.setKiosk(false);
|
|
105
|
+
this.$win.blur();
|
|
106
|
+
this.$win.blurWebView();
|
|
107
|
+
this.$win.unmaximize();
|
|
108
|
+
// this.$win.removeBrowserView(this.$view);
|
|
109
|
+
if (this.singleWindow) {
|
|
110
|
+
this.$win.hide();
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
this.$win.destroy();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 设置语言
|
|
118
|
+
*/
|
|
119
|
+
async setLang(lang) {
|
|
120
|
+
this.logger("setLang", lang);
|
|
121
|
+
// await this.isReady;
|
|
122
|
+
// this.$view.webContents.send('SCREENRECORD:setLang', lang);
|
|
123
|
+
}
|
|
124
|
+
async reset() {
|
|
125
|
+
// 重置选择区域
|
|
126
|
+
this.$win?.webContents.send("SCREENRECORD:reset");
|
|
127
|
+
// 保证 UI 有足够的时间渲染
|
|
128
|
+
await Promise.race([
|
|
129
|
+
new Promise((resolve) => {
|
|
130
|
+
setTimeout(() => resolve(), 500);
|
|
131
|
+
}),
|
|
132
|
+
new Promise((resolve) => {
|
|
133
|
+
electron_1.ipcMain.once("SCREENRECORD:reset", () => resolve());
|
|
134
|
+
}),
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* 初始化窗口
|
|
139
|
+
*/
|
|
140
|
+
async createWindow(display, imageUrl) {
|
|
141
|
+
// 重置截图区域
|
|
142
|
+
await this.reset();
|
|
143
|
+
// 复用未销毁的窗口
|
|
144
|
+
if (!this.$win || this.$win?.isDestroyed?.()) {
|
|
145
|
+
const windowTypes = {
|
|
146
|
+
darwin: "panel",
|
|
147
|
+
// linux 必须设置为 undefined,否则会在部分系统上不能触发focus 事件
|
|
148
|
+
// https://github.com/nashaofu/screenshots/issues/203#issuecomment-1518923486
|
|
149
|
+
linux: undefined,
|
|
150
|
+
win32: "toolbar",
|
|
151
|
+
};
|
|
152
|
+
this.$win = new electron_1.BrowserWindow({
|
|
153
|
+
title: "screenrecord",
|
|
154
|
+
x: display.x,
|
|
155
|
+
y: display.y,
|
|
156
|
+
width: display.width,
|
|
157
|
+
height: display.height,
|
|
158
|
+
useContentSize: true,
|
|
159
|
+
type: windowTypes[process.platform],
|
|
160
|
+
frame: false,
|
|
161
|
+
show: false,
|
|
162
|
+
autoHideMenuBar: true,
|
|
163
|
+
transparent: true,
|
|
164
|
+
resizable: false,
|
|
165
|
+
movable: false,
|
|
166
|
+
minimizable: false,
|
|
167
|
+
maximizable: false,
|
|
168
|
+
// focusable 必须设置为 true, 否则窗口不能及时响应esc按键,输入框也不能输入
|
|
169
|
+
focusable: true,
|
|
170
|
+
skipTaskbar: true,
|
|
171
|
+
alwaysOnTop: true,
|
|
172
|
+
/**
|
|
173
|
+
* linux 下必须设置为false,否则不能全屏显示在最上层
|
|
174
|
+
* mac 下设置为false,否则可能会导致程序坞不恢复问题,且与 kiosk 模式冲突
|
|
175
|
+
*/
|
|
176
|
+
fullscreen: false,
|
|
177
|
+
// mac fullscreenable 设置为 true 会导致应用崩溃
|
|
178
|
+
fullscreenable: false,
|
|
179
|
+
kiosk: true,
|
|
180
|
+
backgroundColor: "#00000000",
|
|
181
|
+
titleBarStyle: "hidden",
|
|
182
|
+
hasShadow: false,
|
|
183
|
+
paintWhenInitiallyHidden: false,
|
|
184
|
+
// mac 特有的属性
|
|
185
|
+
roundedCorners: false,
|
|
186
|
+
enableLargerThanScreen: false,
|
|
187
|
+
acceptFirstMouse: true,
|
|
188
|
+
webPreferences: {
|
|
189
|
+
preload: require.resolve("./preload.js"),
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
this.emit("windowCreated", this.$win);
|
|
193
|
+
this.$win.on("show", () => {
|
|
194
|
+
this.$win?.focus();
|
|
195
|
+
this.$win?.setKiosk(true);
|
|
196
|
+
setTimeout(() => {
|
|
197
|
+
this.$win?.webContents.send("SCREENRECORD:capture", display, imageUrl, process.platform === "linux");
|
|
198
|
+
}, 400);
|
|
199
|
+
});
|
|
200
|
+
// this.$win.on('minimize',(e:MouseEvent)=>{
|
|
201
|
+
// console.log('触发了最小化');
|
|
202
|
+
// e.preventDefault();
|
|
203
|
+
// })
|
|
204
|
+
this.$win.on("closed", () => {
|
|
205
|
+
this.emit("windowClosed", this.$win);
|
|
206
|
+
this.$win = null;
|
|
207
|
+
// this.stopMouseTracking();
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
this.$win
|
|
211
|
+
.loadURL(`file://${require.resolve("eedatek-react-screenrecord/electron/electron.html")}`)
|
|
212
|
+
.catch((reason) => {
|
|
213
|
+
console.log(`clipScreen window failed to load: ${reason}`);
|
|
214
|
+
});
|
|
215
|
+
// 适定平台
|
|
216
|
+
if (process.platform === "darwin") {
|
|
217
|
+
this.$win.setWindowButtonVisibility(false);
|
|
218
|
+
}
|
|
219
|
+
if (process.platform !== "win32") {
|
|
220
|
+
this.$win.setVisibleOnAllWorkspaces(true, {
|
|
221
|
+
visibleOnFullScreen: true,
|
|
222
|
+
skipTransformProcessType: true,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
this.$win.blur();
|
|
226
|
+
this.$win.setBounds(display);
|
|
227
|
+
this.$win.setAlwaysOnTop(true, "screen-saver");
|
|
228
|
+
this.$win.show();
|
|
229
|
+
}
|
|
230
|
+
async capture(display) {
|
|
231
|
+
this.logger("SCREENRECORD:capture");
|
|
232
|
+
try {
|
|
233
|
+
const sources = await electron_1.desktopCapturer.getSources({
|
|
234
|
+
types: ["screen"],
|
|
235
|
+
thumbnailSize: {
|
|
236
|
+
width: display.width * display.scaleFactor,
|
|
237
|
+
height: display.height * display.scaleFactor,
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
let source;
|
|
241
|
+
// Linux系统上,screen.getDisplayNearestPoint 返回的 Display 对象的 id
|
|
242
|
+
// 和这里 source 对象上的 display_id(Linux上,这个值是空字符串) 或 id 的中间部分,都不一致
|
|
243
|
+
// 但是,如果只有一个显示器的话,其实不用判断,直接返回就行
|
|
244
|
+
if (sources.length === 1) {
|
|
245
|
+
[source] = sources;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
source = sources.find((item) => item.display_id === display.id.toString() ||
|
|
249
|
+
item.id.startsWith(`screen:${display.id}:`));
|
|
250
|
+
}
|
|
251
|
+
if (!source) {
|
|
252
|
+
this.logger("SCREENRECORD:capture Can't find screen source. sources: %o, display: %o", sources, display);
|
|
253
|
+
throw new Error("Can't find screen source");
|
|
254
|
+
}
|
|
255
|
+
return source.thumbnail.toDataURL();
|
|
256
|
+
// const { Screenshots: NodeScreenshots } = await import('node-screenshots');
|
|
257
|
+
// const capturer = NodeScreenshots.fromPoint(
|
|
258
|
+
// display.x + display.width / 2,
|
|
259
|
+
// display.y + display.height / 2,
|
|
260
|
+
// );
|
|
261
|
+
// this.logger(
|
|
262
|
+
// 'SCREENRECORD:capture NodeScreenshots.fromPoint arguments %o',
|
|
263
|
+
// display,
|
|
264
|
+
// );
|
|
265
|
+
// this.logger(
|
|
266
|
+
// 'SCREENRECORD:capture NodeScreenshots.fromPoint return %o',
|
|
267
|
+
// capturer
|
|
268
|
+
// ? {
|
|
269
|
+
// id: capturer.id,
|
|
270
|
+
// x: capturer.x,
|
|
271
|
+
// y: capturer.y,
|
|
272
|
+
// width: capturer.width,
|
|
273
|
+
// height: capturer.height,
|
|
274
|
+
// rotation: capturer.rotation,
|
|
275
|
+
// scaleFactor: capturer.scaleFactor,
|
|
276
|
+
// isPrimary: capturer.isPrimary,
|
|
277
|
+
// }
|
|
278
|
+
// : null,
|
|
279
|
+
// );
|
|
280
|
+
// if (!capturer) {
|
|
281
|
+
// throw new Error(`NodeScreenshots.fromDisplay(${display.id}) get null`);
|
|
282
|
+
// }
|
|
283
|
+
// const image = await capturer.capture();
|
|
284
|
+
// return `data:image/png;base64,${image.toString('base64')}`;
|
|
285
|
+
}
|
|
286
|
+
catch (err) {
|
|
287
|
+
this.logger("SCREENRECORD:capture NodeScreenshots capture() error %o", err);
|
|
288
|
+
const sources = await electron_1.desktopCapturer.getSources({
|
|
289
|
+
types: ["screen"],
|
|
290
|
+
thumbnailSize: {
|
|
291
|
+
width: display.width,
|
|
292
|
+
height: display.height,
|
|
293
|
+
},
|
|
294
|
+
});
|
|
295
|
+
let source;
|
|
296
|
+
// Linux系统上,screen.getDisplayNearestPoint 返回的 Display 对象的 id
|
|
297
|
+
// 和这里 source 对象上的 display_id(Linux上,这个值是空字符串) 或 id 的中间部分,都不一致
|
|
298
|
+
// 但是,如果只有一个显示器的话,其实不用判断,直接返回就行
|
|
299
|
+
if (sources.length === 1) {
|
|
300
|
+
[source] = sources;
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
source = sources.find((item) => item.display_id === display.id.toString() ||
|
|
304
|
+
item.id.startsWith(`screen:${display.id}:`));
|
|
305
|
+
}
|
|
306
|
+
if (!source) {
|
|
307
|
+
this.logger("SCREENRECORD:capture Can't find screen source. sources: %o, display: %o", sources, display);
|
|
308
|
+
throw new Error("Can't find screen source");
|
|
309
|
+
}
|
|
310
|
+
return source.thumbnail.toDataURL();
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* 绑定ipc时间处理
|
|
315
|
+
*/
|
|
316
|
+
listenIpc() {
|
|
317
|
+
electron_1.ipcMain.on("SCREENRECORD:ready", () => {
|
|
318
|
+
this.logger("SCREENRECORD:ready");
|
|
319
|
+
this.isReadyState = true;
|
|
320
|
+
});
|
|
321
|
+
/**
|
|
322
|
+
* OK事件 开始录屏
|
|
323
|
+
*/
|
|
324
|
+
electron_1.ipcMain.on("SCREENRECORD:ok", () => {
|
|
325
|
+
this.logger("callback SCREENRECORD:ok");
|
|
326
|
+
this.$win?.setIgnoreMouseEvents(true, { forward: true });
|
|
327
|
+
// if(process.platform === "linux"){
|
|
328
|
+
// this.isRecording= true;
|
|
329
|
+
// this.startMouseTracking();
|
|
330
|
+
// }
|
|
331
|
+
});
|
|
332
|
+
/**
|
|
333
|
+
* error事件 录屏前端相关错误信息
|
|
334
|
+
*/
|
|
335
|
+
electron_1.ipcMain.on("SCREENRECORD:error", (e, ex) => {
|
|
336
|
+
this.logger("callback SCREENRECORD:error");
|
|
337
|
+
this.logger(ex);
|
|
338
|
+
});
|
|
339
|
+
/**
|
|
340
|
+
* CANCEL事件
|
|
341
|
+
*/
|
|
342
|
+
electron_1.ipcMain.on("SCREENRECORD:cancel", () => {
|
|
343
|
+
this.isRecording = false;
|
|
344
|
+
this.logger("callback SCREENRECORD:cancel");
|
|
345
|
+
this.$win?.setIgnoreMouseEvents(false);
|
|
346
|
+
// if(process.platform === "linux"){
|
|
347
|
+
// this.stopMouseTracking();
|
|
348
|
+
// }
|
|
349
|
+
});
|
|
350
|
+
electron_1.ipcMain.on("SCREENRECORD:stop", async (e, data) => {
|
|
351
|
+
this.isRecording = false;
|
|
352
|
+
const event = new event_1.default();
|
|
353
|
+
this.emit("stop", event, data);
|
|
354
|
+
if (event.defaultPrevented || !this.$win) {
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
this.endCapture();
|
|
358
|
+
if (data) {
|
|
359
|
+
const time = new Date();
|
|
360
|
+
const year = time.getFullYear();
|
|
361
|
+
const month = (0, padStart_1.default)(time.getMonth() + 1, 2, "0");
|
|
362
|
+
const date = (0, padStart_1.default)(time.getDate(), 2, "0");
|
|
363
|
+
const hours = (0, padStart_1.default)(time.getHours(), 2, "0");
|
|
364
|
+
const minutes = (0, padStart_1.default)(time.getMinutes(), 2, "0");
|
|
365
|
+
const seconds = (0, padStart_1.default)(time.getSeconds(), 2, "0");
|
|
366
|
+
const milliseconds = (0, padStart_1.default)(time.getMilliseconds(), 3, "0");
|
|
367
|
+
const { canceled, filePath } = await electron_1.dialog.showSaveDialog(this.$win, {
|
|
368
|
+
defaultPath: `${year}${month}${date}${hours}${minutes}${seconds}${milliseconds}.webm`,
|
|
369
|
+
});
|
|
370
|
+
if (canceled || !filePath) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
await fs_extra_1.default.writeFile(filePath, Buffer.from(data));
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
electron_1.ipcMain.handle("SCREENRECORD:get-worker-js", async () => {
|
|
377
|
+
const workerjs = getWorkerJs();
|
|
378
|
+
return workerjs;
|
|
379
|
+
});
|
|
380
|
+
electron_1.ipcMain.on("SCREENRECORD:update-button-round", (e, data) => {
|
|
381
|
+
this.updateButtonBounds(data);
|
|
382
|
+
});
|
|
383
|
+
electron_1.ipcMain.handle("SCREENRECORD:get-desktop-capturer-source", async () => {
|
|
384
|
+
return [
|
|
385
|
+
...(await electron_1.desktopCapturer.getSources({ types: ["screen", "window"] })),
|
|
386
|
+
...(await selfWindws()),
|
|
387
|
+
];
|
|
388
|
+
});
|
|
389
|
+
electron_1.ipcMain.handle("SCREENRECORD:get-display-nearest-point", async () => {
|
|
390
|
+
if (this.$win) {
|
|
391
|
+
// 获取主窗口的位置
|
|
392
|
+
const mainWindowPosition = this.$win.getPosition();
|
|
393
|
+
// 查找包含主窗口位置的屏幕
|
|
394
|
+
const currentScreen = electron_1.screen.getDisplayNearestPoint({
|
|
395
|
+
x: mainWindowPosition[0],
|
|
396
|
+
y: mainWindowPosition[1],
|
|
397
|
+
});
|
|
398
|
+
return currentScreen;
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
exports.default = ScreenRecord;
|
|
404
|
+
const selfWindws = async () => await Promise.all(electron_1.webContents
|
|
405
|
+
.getAllWebContents()
|
|
406
|
+
.filter((item) => {
|
|
407
|
+
const win = electron_1.BrowserWindow.fromWebContents(item);
|
|
408
|
+
return win && win.isVisible();
|
|
409
|
+
})
|
|
410
|
+
.map(async (item) => {
|
|
411
|
+
const win = electron_1.BrowserWindow.fromWebContents(item);
|
|
412
|
+
const thumbnail = await win?.capturePage();
|
|
413
|
+
return {
|
|
414
|
+
name: win?.getTitle() + (item.devToolsWebContents === null ? "" : "-dev"),
|
|
415
|
+
id: win?.getMediaSourceId(),
|
|
416
|
+
thumbnail,
|
|
417
|
+
display_id: "",
|
|
418
|
+
appIcon: null,
|
|
419
|
+
};
|
|
420
|
+
}));
|
|
421
|
+
function getWorkerJs() {
|
|
422
|
+
const protocol = "file";
|
|
423
|
+
const hostname = "";
|
|
424
|
+
const port = "";
|
|
425
|
+
let pathname = path_1.default.resolve(__dirname, `./worker.js`);
|
|
426
|
+
const localUrl = new URL(`${protocol}://${hostname}${port}`);
|
|
427
|
+
localUrl.pathname = pathname;
|
|
428
|
+
return localUrl.href;
|
|
429
|
+
}
|
package/lib/padStart.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* 如果string字符串长度小于 length 则在左侧填充字符
|
|
5
|
+
* 如果超出length长度则截断超出的部分。
|
|
6
|
+
* @param {unknown} string
|
|
7
|
+
* @param {string} chars
|
|
8
|
+
* @param {number} length
|
|
9
|
+
*/
|
|
10
|
+
function padStart(string, length = 0, chars = ' ') {
|
|
11
|
+
let str = String(string);
|
|
12
|
+
while (str.length < length) {
|
|
13
|
+
str = `${chars}${str}`;
|
|
14
|
+
}
|
|
15
|
+
return str;
|
|
16
|
+
}
|
|
17
|
+
exports.default = padStart;
|
package/lib/preload.d.ts
ADDED
package/lib/preload.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/* eslint-disable no-console */
|
|
4
|
+
const electron_1 = require("electron");
|
|
5
|
+
const map = new Map();
|
|
6
|
+
electron_1.contextBridge.exposeInMainWorld('screenshots', {
|
|
7
|
+
ready: () => {
|
|
8
|
+
console.log('contextBridge ready');
|
|
9
|
+
electron_1.ipcRenderer.send('SCREENRECORD:ready');
|
|
10
|
+
},
|
|
11
|
+
reset: () => {
|
|
12
|
+
console.log('contextBridge reset');
|
|
13
|
+
electron_1.ipcRenderer.send('SCREENRECORD:reset');
|
|
14
|
+
},
|
|
15
|
+
save: (arrayBuffer, data) => {
|
|
16
|
+
console.log('contextBridge save', arrayBuffer, data);
|
|
17
|
+
electron_1.ipcRenderer.send('SCREENRECORD:save', Buffer.from(arrayBuffer), data);
|
|
18
|
+
},
|
|
19
|
+
cancel: () => {
|
|
20
|
+
console.log('contextBridge cancel');
|
|
21
|
+
electron_1.ipcRenderer.send('SCREENRECORD:cancel');
|
|
22
|
+
},
|
|
23
|
+
ok: () => {
|
|
24
|
+
console.log('contextBridge ok');
|
|
25
|
+
electron_1.ipcRenderer.send('SCREENRECORD:ok');
|
|
26
|
+
},
|
|
27
|
+
error: (data) => {
|
|
28
|
+
console.log('contextBridge error');
|
|
29
|
+
electron_1.ipcRenderer.send('SCREENRECORD:error', data);
|
|
30
|
+
},
|
|
31
|
+
on: (channel, fn) => {
|
|
32
|
+
console.log('contextBridge on', fn);
|
|
33
|
+
const listener = (event, ...args) => {
|
|
34
|
+
console.log('contextBridge on', channel, fn, ...args);
|
|
35
|
+
fn(...args);
|
|
36
|
+
};
|
|
37
|
+
const listeners = map.get(fn) ?? {};
|
|
38
|
+
listeners[channel] = listener;
|
|
39
|
+
map.set(fn, listeners);
|
|
40
|
+
electron_1.ipcRenderer.on(`SCREENRECORD:${channel}`, listener);
|
|
41
|
+
},
|
|
42
|
+
off: (channel, fn) => {
|
|
43
|
+
console.log('contextBridge off', fn);
|
|
44
|
+
const listeners = map.get(fn) ?? {};
|
|
45
|
+
const listener = listeners[channel];
|
|
46
|
+
delete listeners[channel];
|
|
47
|
+
if (!listener) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
electron_1.ipcRenderer.off(`SCREENRECORD:${channel}`, listener);
|
|
51
|
+
},
|
|
52
|
+
stop: (data) => electron_1.ipcRenderer.send('SCREENRECORD:stop', data),
|
|
53
|
+
invokeGetDesktopCapturerSource: () => {
|
|
54
|
+
return electron_1.ipcRenderer.invoke('SCREENRECORD:get-desktop-capturer-source');
|
|
55
|
+
},
|
|
56
|
+
invokeGetDisplayNearestPoint: () => {
|
|
57
|
+
return electron_1.ipcRenderer.invoke('SCREENRECORD:get-display-nearest-point');
|
|
58
|
+
},
|
|
59
|
+
invokeGetWorker: () => {
|
|
60
|
+
return electron_1.ipcRenderer.invoke('SCREENRECORD:get-worker-js');
|
|
61
|
+
},
|
|
62
|
+
updateButtonBounds: (data) => electron_1.ipcRenderer.send('SCREENRECORD:update-button-round', data)
|
|
63
|
+
});
|
package/lib/worker.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
declare function transform(frame: any, controller: any): void;
|
|
2
|
+
declare function saveImg(videoFrame: any): Promise<void>;
|
|
3
|
+
declare function uploadFile(blob: any): Promise<void>;
|
|
4
|
+
declare let x: number;
|
|
5
|
+
declare let y: number;
|
|
6
|
+
declare let width: number;
|
|
7
|
+
declare let height: number;
|
|
8
|
+
declare let type: string;
|
|
9
|
+
declare let num: number;
|
|
10
|
+
declare let videoFrames: any[];
|
package/lib/worker.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Copyright (c) 2021 The WebRTC project authors. All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* Use of this source code is governed by a BSD-style license
|
|
6
|
+
* that can be found in the LICENSE file in the root of the source
|
|
7
|
+
* tree.
|
|
8
|
+
*/
|
|
9
|
+
let x = 0;
|
|
10
|
+
let y = 0;
|
|
11
|
+
let width = 0;
|
|
12
|
+
let height = 0;
|
|
13
|
+
let type = "";
|
|
14
|
+
let num = 0;
|
|
15
|
+
let videoFrames = [];
|
|
16
|
+
function transform(frame, controller) {
|
|
17
|
+
/* eslint-disable no-undef */
|
|
18
|
+
const newFrame = new VideoFrame(frame, {
|
|
19
|
+
visibleRect: {
|
|
20
|
+
x: x,
|
|
21
|
+
width: width,
|
|
22
|
+
y: y,
|
|
23
|
+
height: height,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
num++;
|
|
27
|
+
if (type == "gif" && num % 3 == 0) {
|
|
28
|
+
saveImg(newFrame);
|
|
29
|
+
}
|
|
30
|
+
controller.enqueue(newFrame);
|
|
31
|
+
frame.close();
|
|
32
|
+
}
|
|
33
|
+
async function saveImg(videoFrame) {
|
|
34
|
+
const canvas = new OffscreenCanvas(videoFrame.displayWidth, videoFrame.displayHeight);
|
|
35
|
+
const context = canvas.getContext("2d");
|
|
36
|
+
context.drawImage(videoFrame, 0, 0);
|
|
37
|
+
canvas.convertToBlob({ type: "image/jpeg" }).then((blob) => {
|
|
38
|
+
uploadFile(blob);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async function uploadFile(blob) {
|
|
42
|
+
let formData = new FormData();
|
|
43
|
+
formData.append("type", "cg");
|
|
44
|
+
formData.append("file", blob);
|
|
45
|
+
const rsp = await fetch("http://localhost:9190/file/cache", {
|
|
46
|
+
method: "POST",
|
|
47
|
+
body: formData,
|
|
48
|
+
});
|
|
49
|
+
const res = await rsp.json();
|
|
50
|
+
if (res.code == 0) {
|
|
51
|
+
videoFrames.push({
|
|
52
|
+
url: `http://localhost:9190/file?url=${res.data}`,
|
|
53
|
+
filePath: res.data,
|
|
54
|
+
index: num / 3,
|
|
55
|
+
duration: 100,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
onmessage = async (event) => {
|
|
60
|
+
const { operation, size, status } = event.data;
|
|
61
|
+
type = event.data.type;
|
|
62
|
+
num = 0;
|
|
63
|
+
if (operation === "crop" && status == "start") {
|
|
64
|
+
const { readable, writable } = event.data;
|
|
65
|
+
x = size.x;
|
|
66
|
+
y = size.y;
|
|
67
|
+
width = size.width;
|
|
68
|
+
height = size.height;
|
|
69
|
+
if (x < 0) {
|
|
70
|
+
width = width + x;
|
|
71
|
+
x = 0;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
x = x % 2 == 0 ? x + 2 : x + 1;
|
|
75
|
+
}
|
|
76
|
+
if (y < 0) {
|
|
77
|
+
height = height + y;
|
|
78
|
+
y = 0;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
y = y % 2 == 0 ? y + 2 : y + 1;
|
|
82
|
+
}
|
|
83
|
+
width = width % 2 == 0 ? width - 6 : width - 5;
|
|
84
|
+
height = height % 2 == 0 ? height - 6 : height - 5;
|
|
85
|
+
readable.pipeThrough(new TransformStream({ transform })).pipeTo(writable);
|
|
86
|
+
}
|
|
87
|
+
else if (status == "stop") {
|
|
88
|
+
console.error("=============================");
|
|
89
|
+
/* eslint-disable no-undef */
|
|
90
|
+
// self.postMessage([...videoFrames]);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
console.error("Unknown operation", operation);
|
|
94
|
+
}
|
|
95
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eedatek-electron-screenrecord",
|
|
3
|
+
"version": "2.0.9",
|
|
4
|
+
"description": "electron 录屏插件",
|
|
5
|
+
"types": "lib/index.d.ts",
|
|
6
|
+
"main": "lib/index.cjs.js",
|
|
7
|
+
"module": "lib/index.js",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib/**"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"prepublishOnly": "npm run build",
|
|
13
|
+
"start": "electron lib/demo.js",
|
|
14
|
+
"dev": "tsc --sourceMap --watch",
|
|
15
|
+
"build": "npm run clean && tsc",
|
|
16
|
+
"lint": "eslint . --ext .js,.ts --fix",
|
|
17
|
+
"clean": "rimraf lib",
|
|
18
|
+
"publishpackage": "npm publish --registry https://registry.npmjs.org/ --access public"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"electron",
|
|
25
|
+
"shortcut",
|
|
26
|
+
"screenshot",
|
|
27
|
+
"cropper"
|
|
28
|
+
],
|
|
29
|
+
"author": "mannuannuan",
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"registry": "https://registry.npmjs.org/"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"debug": "^4.3.4",
|
|
36
|
+
"eedatek-react-screenrecord": "^2.0.9",
|
|
37
|
+
"fs-extra": "^11.1.1"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"electron": ">=14"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/debug": "^4.1.7",
|
|
44
|
+
"@types/fs-extra": "^11.0.1",
|
|
45
|
+
"@types/node": "^18.15.11",
|
|
46
|
+
"@typescript-eslint/eslint-plugin": "^5.57.0",
|
|
47
|
+
"@typescript-eslint/parser": "^5.57.0",
|
|
48
|
+
"cross-env": "^7.0.3",
|
|
49
|
+
"electron": "^23.0.0",
|
|
50
|
+
"eslint": "^8.37.0",
|
|
51
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
52
|
+
"eslint-config-airbnb-typescript": "^17.0.0",
|
|
53
|
+
"eslint-plugin-import": "^2.27.5",
|
|
54
|
+
"rimraf": "^4.4.1",
|
|
55
|
+
"typescript": "^5.0.2"
|
|
56
|
+
}
|
|
57
|
+
}
|