Skip to content

Commit

Permalink
feat: 录制GIF
Browse files Browse the repository at this point in the history
  • Loading branch information
027xiguapi committed Dec 21, 2023
1 parent 88d237b commit ddb201b
Show file tree
Hide file tree
Showing 14 changed files with 241 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/server/src/util/upload.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class UploadMiddleware implements MulterOptionsFactory {
rs: 'webm',
ra: 'webm',
ei: 'png',
gif: 'gif',
};
const type = req.body.type;
const fileType = fileTypeMap[type] || 'webm';
Expand Down
4 changes: 4 additions & 0 deletions packages/web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# @pear-rec/web

## 1.3.7

feat: 录制GIF

## 1.3.6

perf: 优化录制全屏
Expand Down
4 changes: 3 additions & 1 deletion packages/web/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@pear-rec/web",
"private": true,
"version": "1.3.6",
"version": "1.3.7",
"scripts": {
"dev": "vite",
"build": "rimraf dist && tsc && vite build",
Expand All @@ -19,6 +19,7 @@
"dayjs": "^1.11.7",
"eventemitter3": "^5.0.1",
"file-saver": "^2.0.5",
"gif.js": "^0.2.0",
"i18next": "^23.4.6",
"js-cookie": "^3.0.5",
"plyr": "^3.7.8",
Expand All @@ -35,6 +36,7 @@
},
"devDependencies": {
"@types/file-saver": "^2.0.5",
"@types/gif.js": "^0.2.5",
"@types/react": "^18.2.20",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react-swc": "^3.0.0",
Expand Down
Binary file added packages/web/public/video/clip.ogv
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const ScreenRecorder = (props) => {
const audioStream = useRef<MediaStream>(); // 声音流
const mediaRecorder = useRef<MediaRecorder>(); // 媒体录制器对象
const recordedChunks = useRef<Blob[]>([]); // 存储录制的音频数据
const recordedUrl = useRef<string>(''); // 存储录制的音频数据
const [isRecording, setIsRecording] = useState(false); // 标记是否正在录制
const isSave = useRef<boolean>(false);

Expand Down Expand Up @@ -191,6 +192,7 @@ const ScreenRecorder = (props) => {
if (recordedChunks.current.length > 0) {
const blob = new Blob(recordedChunks.current, { type: 'video/webm' });
const url = URL.createObjectURL(blob);
recordedUrl.current = url;
isSave.current = false;
console.log('录屏地址:', url);
recordedChunks.current = [];
Expand All @@ -201,7 +203,6 @@ const ScreenRecorder = (props) => {
async function saveFile(blob) {
try {
recordedChunks.current = [];

const formData = new FormData();
formData.append('type', 'rs');
formData.append('userId', user.id);
Expand Down
132 changes: 132 additions & 0 deletions packages/web/src/components/videoToGif/VideoToGifConverter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import React, { useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import GIF from 'gif.js';
import { Button, Modal, Progress } from 'antd';
import { saveAs } from 'file-saver';
import { useApi } from '../../api';

export default function VideoToGifConverter({ videoSrc, user }) {
const { t } = useTranslation();
const api = useApi();
const gifRef = useRef(null);
const gifBlobRef = useRef(null);
const videoRef = useRef(null);
const [isLoaded, setIsLoaded] = useState(false);
const [percent, setPercent] = useState(0);

const handleConvertClick = async () => {
await videoRef.current.play();
convertToGif();
};

const handlePlayClick = async () => {
await videoRef.current.play();
};

useEffect(() => {
const handleLoadedMetadata = () => {
setIsLoaded(true);
};

videoRef.current.addEventListener('loadedmetadata', handleLoadedMetadata);

return () => {
videoRef.current.removeEventListener('loadedmetadata', handleLoadedMetadata);
};
}, []);

const convertToGif = async () => {
if (!isLoaded) {
return;
}

const canvas = document.createElement('canvas');
canvas.width = videoRef.current.videoWidth;
canvas.height = videoRef.current.videoHeight;
const context = canvas.getContext('2d', { willReadFrequently: true });
const worker = new URL('./gif.js/gif.worker.js', import.meta.url) as any;
// const delay = 1000 / videoRef.current.playbackRate;
const delay = 100;

const gif = new GIF({
workers: 4,
quality: 10,
workerScript: worker,
});

gif.on('finished', (blob) => {
gifBlobRef.current = blob;
const gifUrl = URL.createObjectURL(blob);
gifRef.current.src = gifUrl;
});

gif.on('progress', (progress) => {
setPercent(Math.round(progress * 100));
});

gif.addFrame(canvas, { copy: true, delay: delay });

const renderFrame = () => {
context.drawImage(videoRef.current, 0, 0);
gif.addFrame(canvas, { copy: true, delay: delay });
if (videoRef.current.currentTime < videoRef.current.duration) {
setTimeout(renderFrame, delay);
} else {
gif.render();
}
};
setTimeout(renderFrame, delay);
};

async function handleSaveClick() {
const blob = gifBlobRef.current;
try {
const formData = new FormData();
formData.append('type', 'gif');
formData.append('userId', user.id);
formData.append('file', blob);
const res = (await api.saveFile(formData)) as any;
if (res.code == 0) {
if (window.isElectron) {
window.electronAPI?.sendEiCloseWin();
window.electronAPI?.sendViOpenWin({ imgUrl: res.data.filePath });
} else {
Modal.confirm({
title: '图片已保存,是否查看?',
content: `${res.data.filePath}`,
okText: t('modal.ok'),
cancelText: t('modal.cancel'),
onOk() {
window.open(`/viewImage.html?imgUrl=${res.data.filePath}`);
console.log('OK');
},
onCancel() {
console.log('Cancel');
},
});
}
}
} catch (err) {
saveAs(blob, `pear-rec_${+new Date()}.gif`);
}
}

return (
<div>
<video className="videoRef" ref={videoRef} src={videoSrc}></video>
<div className="tool">
<Progress percent={percent} />
<Button className="playBtn" onClick={handlePlayClick} type="primary" danger>
播放
</Button>
<Button className="convertBtn" onClick={handleConvertClick} type="primary">
保存
</Button>
<Button className="saveBtn" onClick={handleSaveClick}>
下载
</Button>
</div>
<img ref={gifRef} className="hide" alt="GIF" />
</div>
);
}
3 changes: 3 additions & 0 deletions packages/web/src/components/videoToGif/gif.js/gif.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions packages/web/src/components/videoToGif/gif.js/gif.js.map

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions packages/web/src/components/videoToGif/gif.js/gif.worker.js

Large diffs are not rendered by default.

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions packages/web/src/pages/videoToGif.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/x-icon" href="/imgs/logo/favicon.ico" />
<title>pear-rec | GIF</title>
</head>

<body>
<div id="root"></div>
<script type="module" src="./videoToGif/index.tsx"></script>
</body>

</html>
18 changes: 18 additions & 0 deletions packages/web/src/pages/videoToGif/index.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.videoToGif {
background: #fff;
max-width: 1000px;
:global {
.videoRef {
width: 100%;
}
.tool {
text-align: center;
.playBtn {
margin-right: 10px;
}
.convertBtn {
margin-right: 10px;
}
}
}
}
Loading

0 comments on commit ddb201b

Please sign in to comment.