diff --git a/packages/desktop/CHANGELOG.md b/packages/desktop/CHANGELOG.md index a6cdb53c..2c076192 100644 --- a/packages/desktop/CHANGELOG.md +++ b/packages/desktop/CHANGELOG.md @@ -1,5 +1,9 @@ # @pear-rec/desktop +## 1.3.1 + +feat: 增加录全屏 + ## 1.3.0 feat: 增加钉图功能 diff --git a/packages/desktop/electron-builder.json5 b/packages/desktop/electron-builder.json5 index e7e9fb8e..f5f9f08e 100644 --- a/packages/desktop/electron-builder.json5 +++ b/packages/desktop/electron-builder.json5 @@ -3,17 +3,22 @@ */ { appId: 'com.electron.pear', - productName: 'pear-rec', asar: false, + productName: 'pear-rec', + copyright: 'Copyright © 2023 ${author}', // extraResources: ['node_modules/sql.js', 'node_modules/typeorm'], directories: { output: 'release/${version}', }, files: ['dist-electron', 'dist'], mac: { - artifactName: 'pear-rec.${ext}', + artifactName: '${productName}-Mac-${version}-Installer.${ext}', target: ['dmg', 'zip'], }, + linux: { + target: ['AppImage'], + artifactName: '${productName}-Linux-${version}.${ext}', + }, win: { icon: 'build/icons/win/icon.ico', target: [ @@ -22,7 +27,7 @@ arch: ['x64'], }, ], - artifactName: 'pear-rec.${ext}', + artifactName: '${productName}-Windows-${version}-Setup.${ext}', requestedExecutionLevel: 'requireAdministrator', }, nsis: { diff --git a/packages/desktop/electron/main/ipcMain.ts b/packages/desktop/electron/main/ipcMain.ts index 129f9696..c86edbcd 100644 --- a/packages/desktop/electron/main/ipcMain.ts +++ b/packages/desktop/electron/main/ipcMain.ts @@ -12,6 +12,7 @@ import * as viewAudioWin from '../win/viewAudioWin'; import * as settingWin from '../win/settingWin'; import * as recordsWin from '../win/recordsWin'; import * as pinImageWin from '../win/pinImageWin'; +import * as recorderFullScreenWin from '../win/recorderFullScreenWin'; import * as utils from './utils'; import { editConfig } from './config'; @@ -121,7 +122,6 @@ function initIpcMain() { // 录屏截图 ipcMain.on('cs:open-win', () => { clipScreenWin.closeClipScreenWin(); - mainWin.hideMainWin(); clipScreenWin.openClipScreenWin(); }); ipcMain.on('cs:close-win', () => { @@ -234,50 +234,6 @@ function initIpcMain() { viewVideoWin.setAlwaysOnTopViewVideoWin(isAlwaysOnTop); }); - // 打开图片; - ipcMain.handle('vi:get-images', async (event, title) => { - let res = await dialog.showOpenDialog({ - title: title, - buttonLabel: '按此打开文件', - // defaultPath: app.getAppPath("aaa"), - properties: ['multiSelections'], - filters: [ - { name: '图片', extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'] }, - // { name: "视频", extensions: ["mkv", "avi", "mp4"] }, - ], - }); - const images = res.filePaths.map((filePath, index) => { - return { src: `pearrec:///${filePath}`, key: index }; - }); - return images; - }); - ipcMain.handle('vi:get-img', async (event, title) => { - // let res = await dialog.showOpenDialog({ - // title: title, - // buttonLabel: '按此打开文件', - // properties: ['openFile'], - // filters: [{ name: '图片', extensions: ['jpg', 'jpeg', 'png', 'webp', 'svg'] }], - // }); - // res.filePaths.map((filePath, index) => { - // store.setHistoryImg(filePath); - // }); - // const img = await store.getHistoryImg(); - // return img; - }); - // 打开视频 - ipcMain.handle('vv:get-video', async (e) => { - // let res = await dialog.showOpenDialog({ - // title: '选择视频', - // buttonLabel: '按此打开文件', - // properties: ['openFile'], - // filters: [{ name: '视频', extensions: ['mkv', 'avi', 'mp4', 'webm'] }], - // }); - // res.filePaths.map((filePath, index) => { - // store.setHistoryVideo(filePath); - // }); - // const video = await store.getHistoryVideo(); - // return video; - }); // 录音; ipcMain.on('ra:open-win', () => { recorderAudioWin.closeRecorderAudioWin(); @@ -371,7 +327,6 @@ function initIpcMain() { // 钉图 ipcMain.on('pi:open-win', (e, search) => { - console.log(111, search); pinImageWin.openPinImageWin(search); }); ipcMain.on('pi:close-win', () => { @@ -383,6 +338,15 @@ function initIpcMain() { ipcMain.on('pi:maximize-win', () => { pinImageWin.maximizePinImageWin(); }); + + // 录全屏 + ipcMain.on('rfs:open-win', () => { + recorderFullScreenWin.closeRecorderFullScreenWin(); + recorderFullScreenWin.openRecorderFullScreenWin(); + }); + ipcMain.on('rfs:close-win', () => { + recorderFullScreenWin.closeRecorderFullScreenWin(); + }); } initIpcMain(); diff --git a/packages/desktop/electron/preload/electronAPI.ts b/packages/desktop/electron/preload/electronAPI.ts index f7de2e8d..5a0e14c2 100644 --- a/packages/desktop/electron/preload/electronAPI.ts +++ b/packages/desktop/electron/preload/electronAPI.ts @@ -79,7 +79,6 @@ contextBridge.exposeInMainWorld('electronAPI', { //vvWin sendVvOpenWin: (search?: string) => ipcRenderer.send('vv:open-win', search), invokeVvGetHistoryVideo: () => ipcRenderer.invoke('vv:get-historyVideo'), - invokeVvGetVideo: () => ipcRenderer.invoke('vv:get-video'), sendVvSetHistoryVideo: (img: string) => ipcRenderer.send('vv:set-historyVideo', img), //vaWin @@ -103,4 +102,8 @@ contextBridge.exposeInMainWorld('electronAPI', { sendPiCloseWin: () => ipcRenderer.send('pi:close-win'), sendPiMinimizeWin: () => ipcRenderer.send('pi:minimize-win'), sendPiMaximizeWin: () => ipcRenderer.send('pi:maximize-win'), + + // rfs + sendRfsOpenWin: () => ipcRenderer.send('rfs:open-win'), + sendRfsCloseWin: () => ipcRenderer.send('rfs:close-win'), }); diff --git a/packages/desktop/electron/win/recorderFullScreenWin.ts b/packages/desktop/electron/win/recorderFullScreenWin.ts new file mode 100644 index 00000000..761c0ede --- /dev/null +++ b/packages/desktop/electron/win/recorderFullScreenWin.ts @@ -0,0 +1,79 @@ +import { app, BrowserWindow, dialog, shell, screen, Rectangle } from 'electron'; +import { join, basename, dirname } from 'node:path'; +import { preload, url, DIST, ICON, WEB_URL, DIST_ELECTRON } from '../main/contract'; + +const recorderFullScreenHtml = join(DIST, './recordeFullScreen.html'); +let recorderFullScreenWin: BrowserWindow | null = null; + +function createRecorderFullScreenWin(): BrowserWindow { + recorderFullScreenWin = new BrowserWindow({ + title: 'pear-rec 录屏', + icon: ICON, + height: 40, + width: 365, + center: true, + transparent: true, // 使窗口透明 + autoHideMenuBar: true, // 自动隐藏菜单栏 + frame: false, // 无边框窗口 + hasShadow: false, // 窗口是否有阴影 + fullscreenable: false, // 窗口是否可以进入全屏状态 + alwaysOnTop: true, // 窗口是否永远在别的窗口的上面 + skipTaskbar: true, + webPreferences: { + preload, + }, + }); + recorderFullScreenWin?.setBounds({ y: 0 }); + recorderFullScreenWin?.setResizable(false); + if (url) { + recorderFullScreenWin.loadURL(WEB_URL + `recorderFullScreen.html`); + // recorderFullScreenWin.webContents.openDevTools(); + } else { + recorderFullScreenWin.loadFile(recorderFullScreenHtml); + } + + return recorderFullScreenWin; +} + +function closeRecorderFullScreenWin() { + recorderFullScreenWin?.isDestroyed() || recorderFullScreenWin?.close(); + recorderFullScreenWin = null; +} + +function openRecorderFullScreenWin() { + if (!recorderFullScreenWin || recorderFullScreenWin?.isDestroyed()) { + recorderFullScreenWin = createRecorderFullScreenWin(); + } + recorderFullScreenWin?.show(); +} + +function hideRecorderFullScreenWin() { + recorderFullScreenWin?.hide(); +} + +function showRecorderFullScreenWin() { + recorderFullScreenWin?.show(); +} + +function minimizeRecorderFullScreenWin() { + recorderFullScreenWin?.minimize(); +} + +function setMovableRecorderFullScreenWin(movable: boolean) { + recorderFullScreenWin?.setMovable(movable); +} + +function setAlwaysOnTopRecorderFullScreenWin(isAlwaysOnTop: boolean) { + recorderFullScreenWin?.setAlwaysOnTop(isAlwaysOnTop); +} + +export { + createRecorderFullScreenWin, + closeRecorderFullScreenWin, + openRecorderFullScreenWin, + hideRecorderFullScreenWin, + showRecorderFullScreenWin, + minimizeRecorderFullScreenWin, + setMovableRecorderFullScreenWin, + setAlwaysOnTopRecorderFullScreenWin, +}; diff --git a/packages/desktop/package.json b/packages/desktop/package.json index 7d2400b3..b86bb5ab 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -41,8 +41,7 @@ "typescript": "^5.2.2", "uuid": "^9.0.1", "vite": "^4.4.9", - "vite-plugin-electron": "^0.13.0-beta.3", - "vite-plugin-electron-renderer": "^0.14.5" + "vite-plugin-electron": "^0.14.0" }, "engines": { "node": "^14.18.0 || >=16.0.0" diff --git a/packages/desktop/vite.config.ts b/packages/desktop/vite.config.ts index f3223c23..f3b391e7 100644 --- a/packages/desktop/vite.config.ts +++ b/packages/desktop/vite.config.ts @@ -1,8 +1,6 @@ import { rmSync } from 'node:fs'; import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; -import electron from 'vite-plugin-electron'; -import renderer from 'vite-plugin-electron-renderer'; +import electron from 'vite-plugin-electron/simple'; import pkg from './package.json'; // https://vitejs.dev/config/ @@ -15,21 +13,12 @@ export default defineConfig(({ command }) => { return { plugins: [ - react(), - electron([ - { - // Main-Process entry file of the Electron App. + electron({ + main: { entry: 'electron/main/index.ts', - onstart(options) { - if (process.env.VSCODE_DEBUG) { - console.log(/* For `.vscode/.debug.script.mjs` */ '[startup] Electron App'); - } else { - options.startup(); - } - }, vite: { build: { - sourcemap, + sourcemap: sourcemap ? 'inline' : undefined, // #332 minify: isBuild, outDir: 'dist-electron/main', rollupOptions: { @@ -38,13 +27,8 @@ export default defineConfig(({ command }) => { }, }, }, - { - entry: 'electron/preload/index.ts', - onstart(options) { - // Notify the Renderer-Process to reload the page when the Preload-Scripts build is complete, - // instead of restarting the entire Electron App. - options.reload(); - }, + preload: { + input: 'electron/preload/index.ts', vite: { build: { sourcemap: sourcemap ? 'inline' : undefined, // #332 @@ -56,9 +40,7 @@ export default defineConfig(({ command }) => { }, }, }, - ]), - // Use Node.js API in the Renderer-process - renderer(), + }), ], server: process.env.VSCODE_DEBUG && diff --git a/packages/web/CHANGELOG.md b/packages/web/CHANGELOG.md index b2b86183..314f2e1e 100644 --- a/packages/web/CHANGELOG.md +++ b/packages/web/CHANGELOG.md @@ -1,5 +1,9 @@ # @pear-rec/web +## 1.3.1 + +feat: 增加录全屏 + ## 1.3.0 feat: 增加钉图功能 diff --git a/packages/web/package.json b/packages/web/package.json index 25879626..68abb70e 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -7,8 +7,7 @@ "build": "rimraf dist && tsc && vite build", "watch": "tsc && vite build --mode lib -w", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "rimraf dist && tsc && vite build && vite preview", - "deploy": "deploy.sh" + "preview": "vite preview" }, "dependencies": { "@pear-rec/recorder": "workspace:^", diff --git a/packages/web/src/components/card/recordScreenCard.tsx b/packages/web/src/components/card/recordScreenCard.tsx index 24948b97..722e83ec 100644 --- a/packages/web/src/components/card/recordScreenCard.tsx +++ b/packages/web/src/components/card/recordScreenCard.tsx @@ -1,53 +1,39 @@ import React, { useImperativeHandle, forwardRef } from 'react'; import { useTranslation } from 'react-i18next'; import { CameraOutlined, DownOutlined } from '@ant-design/icons'; -import { Card, Space, Dropdown } from 'antd'; -import type { MenuProps } from 'antd'; +import { Card, Space, Button } from 'antd'; const RecordScreenCard = forwardRef((props: any, ref: any) => { useImperativeHandle(ref, () => ({})); const { t } = useTranslation(); function handleClipScreen() { - window.electronAPI - ? window.electronAPI.sendCsOpenWin() - : (location.href = '/recorderScreen.html'); - } - - function handleRecordScreen() { - window.electronAPI - ? window.electronAPI.sendRsOpenWin({ isFullScreen: true }) - : (location.href = '/recorderScreen.html'); + if (window.isElectron) { + window.electronAPI.sendCsOpenWin(); + window.electronAPI.sendMaCloseWin(); + } else { + location.href = '/recorderFullScreen.html'; + } } - const onClick: MenuProps['onClick'] = ({ key }) => { - if (key == 'full') { - handleRecordScreen(); + function handleFullScreenClick() { + if (window.isElectron) { + window.electronAPI.sendRfsOpenWin(); + window.electronAPI.sendMaCloseWin(); } else { - handleClipScreen(); + location.href = '/recorderFullScreen.html'; } - }; - - const items: MenuProps['items'] = [ - { - label: '全屏录制', - key: 'full', - }, - { - label: '局部录制', - key: 'clip', - }, - ]; + } return ( + + {t('home.fullScreen')} +
- {/* */} - {/* */} - {/* */}
{t('home.screenRecording')}
diff --git a/packages/web/src/pages/home/index.module.scss b/packages/web/src/pages/home/index.module.scss index 0d67d67b..cfd349b8 100644 --- a/packages/web/src/pages/home/index.module.scss +++ b/packages/web/src/pages/home/index.module.scss @@ -11,6 +11,12 @@ text-align: center; } } + .extra { + position: absolute; + right: 15px; + top: 10px; + color: #1677ff; + } .cardContent { text-align: center; font-size: 30px; diff --git a/packages/web/src/pages/index.scss b/packages/web/src/pages/index.scss index 0887de32..0f69df29 100644 --- a/packages/web/src/pages/index.scss +++ b/packages/web/src/pages/index.scss @@ -23,15 +23,15 @@ input, textarea, th, td { - margin: 0; - padding: 0; + margin: 0; + padding: 0; } body, button, input, select, textarea { - font: 12px/1.5tahoma, arial, \5b8b\4f53; + font: 12px/1.5tahoma, arial, \5b8b\4f53; } h1, h2, @@ -39,180 +39,179 @@ h3, h4, h5, h6 { - font-size: 100%; + font-size: 100%; } address, cite, dfn, em, var { - font-style: normal; + font-style: normal; } code, kbd, pre, samp { - font-family: couriernew, courier, monospace; + font-family: couriernew, courier, monospace; } small { - font-size: 12px; + font-size: 12px; } ul, ol { - list-style: none; + list-style: none; } a { - text-decoration: none; + text-decoration: none; } a:hover { - text-decoration: underline; + text-decoration: underline; } sup { - vertical-align: text-top; + vertical-align: text-top; } sub { - vertical-align: text-bottom; + vertical-align: text-bottom; } legend { - color: #000; + color: #000; } fieldset, img { - border: 0; + border: 0; } button, input, select, textarea { - font-size: 100%; + font-size: 100%; } table { - border-collapse: collapse; - border-spacing: 0; + border-collapse: collapse; + border-spacing: 0; } .hide { - display: none; + display: none; } .hidden { - display: none; + display: none; } .show { - display: block; + display: block; } @keyframes blink { - 0% { - opacity: 1; - } - 50% { - opacity: 1; - } - 50.01% { - opacity: 0; - } - 100% { - opacity: 0; - } + 0% { + opacity: 1; + } + 50% { + opacity: 1; + } + 50.01% { + opacity: 0; + } + 100% { + opacity: 0; + } } @-webkit-keyframes blink { - 0% { - opacity: 1; - } - 50% { - opacity: 1; - } - 50.01% { - opacity: 0; - } - 100% { - opacity: 0; - } + 0% { + opacity: 1; + } + 50% { + opacity: 1; + } + 50.01% { + opacity: 0; + } + 100% { + opacity: 0; + } } .blink { - animation: blink 1s linear infinite; + color: red; + animation: blink 1s linear infinite; } #root { - display: flex; - justify-content: center; - .viewImgs { - .viewer-toolbar { - background-color: rgba(0, 0, 0, 0.1); - height: 45px; - ul { - padding: 0; - margin: 0 auto; - li { - background-color: transparent; - width: 40px; - height: 45px; - line-height: 45px; - font-size: 16px; - border-radius: unset; - } - li::before { - margin: 0 auto; - margin-top: 15px; - } - li:focus { - box-shadow: unset; - } - .viewer-large { - margin: 0; - } - .viewer-download { - background: transparent url(/imgs/svg/download.svg) no-repeat 0px 0px; - background-size: 20px; - background-position: 8px 14px; - } - .viewer-print { - background: transparent url(/imgs/svg/print.svg) no-repeat 0px 0px; - background-size: 20px; - background-position: 8px 14px; - } - .viewer-always-on-top-win { - background: transparent url(/imgs/svg/pushpin.svg) no-repeat 0px 0px; - background-size: 20px; - background-position: 8px 14px; - } - .viewer-edit { - background: transparent url(/imgs/svg/edit.svg) no-repeat 0px 0px; - background-size: 20px; - background-position: 8px 14px; - } - .viewer-file { - background: transparent url(/imgs/svg/file.svg) no-repeat 0px 0px; - background-size: 20px; - background-position: 8px 14px; - } - .viewer-scan { - background: transparent url(/imgs/svg/viewer-scan.svg) no-repeat 0px - 0px; - background-size: 20px; - background-position: 8px 14px; - } - .viewer-search { - background: transparent url(/imgs/svg/viewer-search.svg) no-repeat 0px - 0px; - background-size: 20px; - background-position: 8px 14px; - } - .current { - background-color: #1677ff; - } - } - } - .viewer-button.viewer-fullscreen { - display: none; - } - } + display: flex; + justify-content: center; + .viewImgs { + .viewer-toolbar { + background-color: rgba(0, 0, 0, 0.1); + height: 45px; + ul { + padding: 0; + margin: 0 auto; + li { + background-color: transparent; + width: 40px; + height: 45px; + line-height: 45px; + font-size: 16px; + border-radius: unset; + } + li::before { + margin: 0 auto; + margin-top: 15px; + } + li:focus { + box-shadow: unset; + } + .viewer-large { + margin: 0; + } + .viewer-download { + background: transparent url(/imgs/svg/download.svg) no-repeat 0px 0px; + background-size: 20px; + background-position: 8px 14px; + } + .viewer-print { + background: transparent url(/imgs/svg/print.svg) no-repeat 0px 0px; + background-size: 20px; + background-position: 8px 14px; + } + .viewer-always-on-top-win { + background: transparent url(/imgs/svg/pushpin.svg) no-repeat 0px 0px; + background-size: 20px; + background-position: 8px 14px; + } + .viewer-edit { + background: transparent url(/imgs/svg/edit.svg) no-repeat 0px 0px; + background-size: 20px; + background-position: 8px 14px; + } + .viewer-file { + background: transparent url(/imgs/svg/file.svg) no-repeat 0px 0px; + background-size: 20px; + background-position: 8px 14px; + } + .viewer-scan { + background: transparent url(/imgs/svg/viewer-scan.svg) no-repeat 0px 0px; + background-size: 20px; + background-position: 8px 14px; + } + .viewer-search { + background: transparent url(/imgs/svg/viewer-search.svg) no-repeat 0px 0px; + background-size: 20px; + background-position: 8px 14px; + } + .current { + background-color: #1677ff; + } + } + } + .viewer-button.viewer-fullscreen { + display: none; + } + } - .aplayer .aplayer-list { - max-height: unset !important; - ol { - max-height: unset !important; - } - } + .aplayer .aplayer-list { + max-height: unset !important; + ol { + max-height: unset !important; + } + } } diff --git a/packages/web/src/pages/recorderFullScreen.html b/packages/web/src/pages/recorderFullScreen.html new file mode 100644 index 00000000..c260ef43 --- /dev/null +++ b/packages/web/src/pages/recorderFullScreen.html @@ -0,0 +1,16 @@ + + + + + + + + pear-rec | 录屏 + + + +
+ + + + \ No newline at end of file diff --git a/packages/web/src/pages/recorderFullScreen/index.module.scss b/packages/web/src/pages/recorderFullScreen/index.module.scss new file mode 100644 index 00000000..62343782 --- /dev/null +++ b/packages/web/src/pages/recorderFullScreen/index.module.scss @@ -0,0 +1,43 @@ +.recorderFullScreen { + display: flex; + align-items: center; + border-radius: 5px; + background-color: rgba(#eee, 0.1); + + :global { + .recordIcon { + font-size: 18px; + width: 32px; + padding: 4px 0; + } + .ant-btn { + padding-top: 5px; + .ant-btn-icon .anticon { + font-size: 18px; + } + } + .singleDigit { + background-color: transparent; + } + .recorderTools { + display: flex; + align-items: center; + .pauseBtn, + .stopBtn, + .playBtn { + padding-top: 2px; + color: #1677ff; + .ant-btn-icon { + font-size: 26px; + } + } + .stopBtn { + color: red; + } + } + } +} + +.recorderFullScreen:hover { + background-color: rgba(#fff, 0.5); +} diff --git a/packages/web/src/pages/recorderFullScreen/index.tsx b/packages/web/src/pages/recorderFullScreen/index.tsx new file mode 100644 index 00000000..93ea243c --- /dev/null +++ b/packages/web/src/pages/recorderFullScreen/index.tsx @@ -0,0 +1,316 @@ +import React, { useState, useEffect, useRef } from 'react'; +import useTimer from '@pear-rec/timer/src/useTimer'; +import { useTranslation } from 'react-i18next'; +import { CameraOutlined } from '@ant-design/icons'; +import { + BsMic, + BsMicMute, + BsPlayFill, + BsPause, + BsFillStopFill, + BsRecordCircle, +} from 'react-icons/bs'; +import { SettingOutlined, CloseOutlined } from '@ant-design/icons'; +import { Button, Modal, message } from 'antd'; +import Timer from '@pear-rec/timer'; +import ininitApp from '../../pages/main'; +import { useApi } from '../../api'; +import { useUserApi } from '../../api/user'; +import '@pear-rec/timer/src/Timer/index.module.scss'; +import styles from './index.module.scss'; + +const RecorderScreen = () => { + const { t } = useTranslation(); + const api = useApi(); + const userApi = useUserApi(); + const videoRef = useRef(); + const mediaStream = useRef(); + const mediaRecorder = useRef(); // 媒体录制器对象 + const recordedChunks = useRef([]); // 存储录制的音频数据 + const audioTrack = useRef(); // 音频轨道对象 + const [isPause, setIsPause] = useState(false); // 标记是否暂停 + const [isRecording, setIsRecording] = useState(false); // 标记是否正在录制 + const [isMute, setIsMute] = useState(false); // 标记是否静音 + const [isSave, setIsSave] = useState(false); // 标记是否正在保存 + const timer = useTimer(); + const [user, setUser] = useState({} as any); + + useEffect(() => { + user.id || getCurrentUser(); + }, []); + + async function getCurrentUser() { + try { + const res = (await userApi.getCurrentUser()) as any; + if (res.code == 0) { + setUser(res.data); + } + } catch (err) { + console.log(err); + } + } + + function handleStartRecording() { + startRecordingElectron(); + } + + function startRecordingWeb() { + navigator.mediaDevices + .getDisplayMedia({ video: true, audio: true }) + .then((stream) => { + videoRef.current!.srcObject = stream; + mediaStream.current = stream; + audioTrack.current = stream.getAudioTracks()[0]; + audioTrack.current.enabled = true; // 开启音频轨道 + mediaRecorder.current = new MediaRecorder(stream); + mediaRecorder.current.addEventListener('dataavailable', (e) => { + if (e.data.size > 0) { + recordedChunks.current.push(e.data); + } + }); + mediaRecorder.current.addEventListener('stop', () => { + exportRecording(); + }); + mediaRecorder.current.start(); + setIsRecording(true); + timer.start(); + console.log('开始录像...'); + }) + .catch((error) => { + console.error('无法获取媒体权限:', error); + }); + } + + // 静音录制 + function muteRecording() { + if (audioTrack.current) { + audioTrack.current.enabled = false; // 关闭音频轨道 + setIsMute(true); + console.log('录像已静音'); + } + } + + // 取消静音 + function unmuteRecording() { + if (audioTrack.current) { + audioTrack.current.enabled = true; // 开启音频轨道 + setIsMute(false); + console.log('取消静音'); + } + } + + // 暂停录制 + function pauseRecording() { + if (!isPause && mediaRecorder.current.state === 'recording') { + mediaRecorder.current.pause(); + setIsPause(true); + timer.pause(); + window.electronAPI?.sendRsPauseRecord(); + console.log('录像已暂停'); + } + } + + // 恢复录制 + function resumeRecording() { + if (isPause && mediaRecorder.current.state === 'paused') { + mediaRecorder.current.resume(); + setIsPause(false); + timer.start(); + window.electronAPI?.sendRsStartRecord(); + console.log('恢复录像...'); + } + } + + // 停止录制,并将录制的音频数据导出为 Blob 对象 + function stopRecording() { + if (isRecording) { + mediaRecorder.current.stop(); + mediaStream.current?.getTracks().forEach((track) => track.stop()); + setIsRecording(false); + timer.reset(); + recordedChunks.current = []; + window.electronAPI?.sendRsStopRecord(); + console.log('录像完成!'); + } + } + // 导出录制的音频文件 + function saveRecording() { + setIsSave(true); + stopRecording(); + } + + // 导出录制的音频文件 + function exportRecording() { + if (recordedChunks.current.length > 0) { + const blob = new Blob(recordedChunks.current, { + type: 'video/webm', + }); + saveFile(blob); + } + } + + async function saveFile(blob) { + try { + recordedChunks.current = []; + setIsSave(false); + const formData = new FormData(); + formData.append('type', 'rs'); + formData.append('userUuid', user.uuid); + formData.append('file', blob); + const res = (await api.saveFile(formData)) as any; + if (res.code == 0) { + window.electronAPI?.sendRfsCloseWin(); + window.electronAPI?.sendVvOpenWin({ videoUrl: res.data.filePath }); + } + } catch (err) { + message.error('保存失败'); + } + } + + function handleToggleMute() { + isMute ? unmuteRecording() : muteRecording(); + } + + async function startRecordingElectron() { + const sources = await window.electronAPI?.invokeRsGetDesktopCapturerSource(); + const source = sources.filter((e: any) => e.id == 'screen:0:0')[0]; + const constraints: any = { + audio: false, + video: { + mandatory: { + chromeMediaSource: 'desktop', + chromeMediaSourceId: source.id, + }, + }, + }; + navigator.mediaDevices + .getUserMedia(constraints) + .then((stream) => { + mediaStream.current = stream; + mediaRecorder.current = new MediaRecorder(stream); + mediaRecorder.current.addEventListener('dataavailable', (e) => { + if (e.data.size > 0) { + recordedChunks.current.push(e.data); + } + }); + mediaRecorder.current.addEventListener('stop', () => { + exportRecording(); + }); + mediaRecorder.current.start(); + setIsRecording(true); + timer.start(); + console.log('开始录像...'); + }) + .catch((error) => { + console.error('无法获取媒体权限:', error); + }); + return constraints; + } + + function handleOpenSettingWin() { + window.electronAPI ? window.electronAPI.sendSeOpenWin() : (location.href = '/setting.html'); + } + + function handleShotScreen() { + if (window.electronAPI) { + window.electronAPI.sendRsShotScreen(); + } else { + const canvas = document.createElement('canvas'); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + const context = canvas.getContext('2d'); + context.drawImage(videoRef.current, 0, 0, canvas.width, canvas.height); + const url = canvas.toDataURL('image/png'); + const link = document.createElement('a'); + link.href = url; + link.download = `pear-rec_${+new Date()}.png`; + link.click(); + } + } + + function handleTogglePause() { + isPause ? resumeRecording() : pauseRecording(); + } + + function handleCloseWin() { + window.electronAPI?.sendRfsCloseWin(); + } + + return ( +
+ + + + {/* */} + +
+ {isSave ? ( + + ) : isRecording ? ( + <> + + + )} +
+
+ ); +}; + +ininitApp(RecorderScreen); +export default RecorderScreen; diff --git a/packages/web/src/pages/recorderScreen/index.module.scss b/packages/web/src/pages/recorderScreen/index.module.scss index ac771d1f..0051d8f3 100644 --- a/packages/web/src/pages/recorderScreen/index.module.scss +++ b/packages/web/src/pages/recorderScreen/index.module.scss @@ -1,94 +1,91 @@ .recorderScreen { - width: 100vw; - height: 100vh; - overflow: hidden; - :global { - .selectMedia { - position: absolute; - left: 50%; - transform: translateX(-50%); - margin-top: 50px; - z-index: 100; - } - .cropArea { - position: absolute; - z-index: 10; - outline: 2px solid #ccc; - #innerCropArea { - height: 100%; - width: 100%; - } - .react-resizable-handle { - background-image: none; - } - } - .screenRecorder { - position: absolute; - z-index: 100; - outline: 2px solid #ccc; - display: flex; - cursor: pointer; - background-color: #ccc; - .toolbarIcon { - font-size: 30px; - line-height: 30px; - } - .blink { - color: red; - } - .recordBtn { - font-size: 18px; - } - .settingBtn { - line-height: 15px; - } - .shotScreenBtn { - line-height: 15px; - } - .drgan { - flex: auto; - } - .playBtn { - color: #1677ff; - } - .stopBtn { - color: red; - } - .pauseBtn { - color: #000; - } - .toggleMuteBtn { - font-size: 18px; - color: #000; - } - .singleDigit { - background: transparent; - } - .playRecorder { - line-height: 40px; - display: flex; - .toolbarTitle { - height: 30px; - line-height: 35px; - } - } - } - .iframeRef { - position: absolute; - left: 0; - top: 0; - } - .videoRef { - width: 100%; - height: 100%; - position: absolute; - left: 0; - top: 0; - } - .control { - position: absolute; - right: 0; - bottom: 0; - } - } + width: 100vw; + height: 100vh; + overflow: hidden; + :global { + .selectMedia { + position: absolute; + left: 50%; + transform: translateX(-50%); + margin-top: 50px; + z-index: 100; + } + .cropArea { + position: absolute; + z-index: 10; + outline: 2px solid #ccc; + #innerCropArea { + height: 100%; + width: 100%; + } + .react-resizable-handle { + background-image: none; + } + } + .screenRecorder { + position: absolute; + z-index: 100; + outline: 2px solid #ccc; + display: flex; + cursor: pointer; + background-color: #ccc; + .toolbarIcon { + font-size: 30px; + line-height: 30px; + } + .recordBtn { + font-size: 18px; + } + .settingBtn { + line-height: 15px; + } + .shotScreenBtn { + line-height: 15px; + } + .drgan { + flex: auto; + } + .playBtn { + color: #1677ff; + } + .stopBtn { + color: red; + } + .pauseBtn { + color: #000; + } + .toggleMuteBtn { + font-size: 18px; + color: #000; + } + .singleDigit { + background: transparent; + } + .playRecorder { + line-height: 40px; + display: flex; + .toolbarTitle { + height: 30px; + line-height: 35px; + } + } + } + .iframeRef { + position: absolute; + left: 0; + top: 0; + } + .videoRef { + width: 100%; + height: 100%; + position: absolute; + left: 0; + top: 0; + } + .control { + position: absolute; + right: 0; + bottom: 0; + } + } } diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index c9433d6b..2561159b 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -10,6 +10,7 @@ const buildOptionsProject = { index: resolve(__dirname, 'src/pages/index.html'), shotScreen: resolve(__dirname, 'src/pages/shotScreen.html'), recorderScreen: resolve(__dirname, 'src/pages/recorderScreen.html'), + recorderFullScreen: resolve(__dirname, 'src/pages/recorderFullScreen.html'), recorderVideo: resolve(__dirname, 'src/pages/recorderVideo.html'), recorderAudio: resolve(__dirname, 'src/pages/recorderAudio.html'), viewImage: resolve(__dirname, 'src/pages/viewImage.html'),