最近遇到个开发需求:在显示器显示监控摄像头的实时画面,要求可以录制GIF
动图,然后现在它,这里只是个基本的先行demo
准备工作
本文主要是借助navigator.mediaDevices
对象下的getUserMedia
方法实现的,他会返回MediaStream
数据流,将它接入video
标签就可以展示实时监控的画面了。
不过如果你需要授予浏览器相机权限,尤其
windows
,仅仅在这个标签页授予权限是没有用的
定义MediaStream
函数
import { useState, useEffect } from "react"; export const useMediaStream = (options: MediaStreamConstraints) => { const [mediaStream, setMediaStream] = useState<MediaStream>(); const [error, setError] = useState<any>(); useEffect(() => { navigator.mediaDevices .getUserMedia( options ?? { audio: false, video: { width: 600, height: 300 } } ) .then(function (mediaStream) { setMediaStream(mediaStream); }) .catch(function (err) { setError(err); }); }, []); return { mediaStream, error }; };
接入Video
中
我试过多种写法,这种是最稳妥的,如果你的video标签一直闪烁,那可能是一直被初始化导致的,这样写就可以避免这种问题。
export const App = () => { const [dom, setDom] = useState<HTMLVideoElement>(); const { mediaStream } = useMediaStream({ audio: false, video: { width: 1920, height: 1080 } }); // 初始化播放器 useEffect(() => { if (!dom) return; dom.srcObject = mediaStream; dom.onloadeddata = function () { dom.play(); }; }, [mediaStream, dom]); // 初始化dom useEffect(() => { if (document.querySelector("video")) { setDom(document.querySelector("video")); } }, []); return <video width={400} height={300} /> }
实现播放和暂停
这是HTMLVideoElement
的方法,直接调用就好了
return ( <> <video poster={posterUrl} width={400} height={300} /> <br /> <button onClick={() => dom.pause()}>暂停</button> <button onClick={() => dom.play()}>播放</button> </> )
此时如下
实现截图和预览截图功能
截图的功能是通过获取当前帧的数据,然后创建img
,生成base64 URL
。接着,展示它就可以预览了
const [cuttImgRUL, setCurrentRUL] = useState<string>(posterUrl); // 获取当前帧画面的dataURL const getCurrenScreen = () => { const canvas = document.createElement("canvas"); canvas.width = dom.videoWidth; canvas.height = dom.videoHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(dom, 0, 0, canvas.width, canvas.height); const dataURL = canvas.toDataURL("image/png"); return dataURL; }; // 设置当前帧 const getPic = () => { setCurrentRUL(getCurrenScreen()); return getCurrenScreen(); }; // 展示截图 const showPic = () => { const dom = document.querySelector("img"); if (!dom) return; dom.src = cuttImgRUL as string; }; // 每当当前帧的地址发生变化就展示它 useEffect(() => showPic(), [cuttImgRUL]); <button onClick={() => downPic()}>下载截图</button> <img width="500px" alt="img" />
实现下载截图
基础的下载操作,就不多说了。
// 下载截图 const downPic = () => { const a = document.createElement("a"); a.href = getCurrenScreen(); a.download = "img"; a.click(); a.remove(); }; <button onClick={() => downPic()}>下载截图</button>
下载如下
录制GIF
每过一段时间获取当前帧的数据,并创建成img
对象,将它们的base64 URL
缓存起来。
/ 录制标记 const [flag, setFlag] = useState<boolean>(false); // GIF的缓存 const [imgs, setImgs] = useState([]); // 录制GIF const getGif = () => { setFlag(true); setImgs([]); const id = setInterval(() => { const img = getPic(); setImgs((curr) => [...curr, img]); }, 16.67); setTimeout(() => { setFlag(false); clearInterval(id); }, 5000); }; <button onClick={() => getGif()}>{flag ? "录制中..." : "录制GIF"}</button>
预览GIF
将缓存的图片快照在图片容器中依次播放,所以这里是个障眼法
// 预览GIF const preViewGif = () => { const newImage = document.querySelector("img"); let idx = 0; const id = setInterval(() => { newImage.src = imgs[idx]; idx++; if (idx === imgs.length - 1) clearInterval(id); }, 200); }; <button onClick={() => preViewGif()}>预览GIF</button>
预览如下
下载GIF
下载GIF的原理是将多个png的图片放入Canvas里,然后在合并成Gif文件并下载,这里用到了工具库gifshot
// 下载GIF const downGif = () => { const config = { fps: 10, width: 1920, height: 1080, images: imgs }; gifshot.createGIF(config, function (obj) { if (!obj.error) { const url = obj.image; // 下载GIF const a = document.createElement("a"); a.href = url; a.download = "img"; a.click(); a.remove(); } }); }; <button onClick={() => downGif()}>下载GIF</button>
下面是下载的其中一个GIF
好了,这就是个简单的demo
,比较简陋,希望能给大家一点思路。最后附上完整的代码
完整代码如下
import { useEffect, useState } from "react"; import { useMediaStream } from "./useMediaStream"; import gifshot from "gifshot"; const posterUrl = "https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ebdfb2583e3f4a33a72bc6ffd44b42f2~tplv-k3u1fbpfcp-zoom-crop-mark:1512:1512:1512:851.awebp?"; export default () => { const [dom, setDom] = useState<HTMLVideoElement>(); // 录制标记 const [flag, setFlag] = useState<boolean>(false); const [cuttImgRUL, setCurrentRUL] = useState<string>(posterUrl); // GIF的缓存 const [imgs, setImgs] = useState([]); // 获取数据流 const { mediaStream } = useMediaStream({ audio: false, video: { width: 1920, height: 1080 } }); // 获取当前帧画面的dataURL const getCurrenScreen = () => { const canvas = document.createElement("canvas"); canvas.width = dom.videoWidth; canvas.height = dom.videoHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(dom, 0, 0, canvas.width, canvas.height); const dataURL = canvas.toDataURL("image/png"); return dataURL; }; // 设置当前帧 const getPic = () => { setCurrentRUL(getCurrenScreen()); return getCurrenScreen(); }; // 下载截图 const downPic = () => { const a = document.createElement("a"); a.href = getCurrenScreen(); a.download = "img"; a.click(); a.remove(); }; // 下载GIF const downGif = () => { const config = { fps: 10, width: 1920, height: 1080, images: imgs }; gifshot.createGIF(config, function (obj) { if (!obj.error) { const url = obj.image; // 下载GIF const a = document.createElement("a"); a.href = url; a.download = "img"; a.click(); a.remove(); } }); }; // 展示截图 const showPic = () => { const dom = document.querySelector("img"); if (!dom) return; dom.src = cuttImgRUL as string; }; // 录制GIF const getGif = () => { setFlag(true); setImgs([]); const id = setInterval(() => { const img = getPic(); setImgs((curr) => [...curr, img]); }, 16.67); setTimeout(() => { setFlag(false); clearInterval(id); }, 5000); }; // 预览GIF const preViewGif = () => { const newImage = document.querySelector("img"); let idx = 0; const id = setInterval(() => { newImage.src = imgs[idx]; idx++; if (idx === imgs.length - 1) clearInterval(id); }, 200); }; // 初始化播放器 useEffect(() => { if (!dom) return; dom.srcObject = mediaStream; dom.onloadeddata = function () { dom.play(); }; }, [mediaStream, dom]); // 初始化dom useEffect(() => { if (document.querySelector("video")) { setDom(document.querySelector("video")); } }, []); // 每当当前帧的地址发生变化就展示它 useEffect(() => showPic(), [cuttImgRUL]); return ( <> <video poster={posterUrl} width={400} height={300} /> <br /> <button onClick={() => dom.pause()}>暂停</button> <button onClick={() => dom.play()}>播放</button> <button onClick={() => getPic()}>截屏</button> <button onClick={() => downPic()}>下载截图</button> <button onClick={() => getGif()}>{flag ? "录制中..." : "录制GIF"}</button> {imgs.length && !flag ? ( <> <button onClick={() => preViewGif()}>预览GIF</button> <button onClick={() => downGif()}>下载GIF</button> </> ) : ( "" )} <br /> <br /> <img width="500px" alt="img" /> <canvas width="500px" height="300px" /> </> ); };