import CameraPreviewBase, { IVideoConf } from "./CameraPreviewBase"; import { IMAGE_TYPE } from "../enum"; export interface IH5VideoParams { video: HTMLVideoElement; canvas?: HTMLCanvasElement; videoConf?: IVideoConf; } export type { IVideoConf }; export class CameraPreviewVideo extends CameraPreviewBase { videoConf: IVideoConf; video: HTMLVideoElement; canvas: HTMLCanvasElement; mediaStream?: MediaStream; isPlaying = false; constructor(params: IH5VideoParams) { super(params); const { videoConf, video, canvas } = params || {}; this.videoConf = { ...videoConf }; this.video = video; if (canvas) { this.canvas = canvas; } else { this.canvas = document.createElement("canvas"); } } /** * 开启视频 * @returns */ async start() { if (this.isPlaying) { return Promise.reject(new Error("video is playing!")); } return new Promise((resolve, reject) => { navigator.mediaDevices .getUserMedia({ audio: false, video: { facingMode: "user", ...this.videoConf, }, }) .then((mediaStream) => { const { video } = this; video.srcObject = mediaStream; this.mediaStream = mediaStream; video.onloadedmetadata = (e) => { video.play(); }; video.onplay = () => { this.isPlaying = true; }; video.onplaying = () => { this.isPlaying = true; resolve(); }; }) .catch(function (error) { console.error("无法访问摄像头: " + error); reject(error); }); }); } /** * 截图 返回 canvas * @returns HTMLCanvasElement */ getSnap(): Promise { return Promise.resolve(this.getSnapCanvas()); } /** * 截图 返回 canvas * @returns HTMLCanvasElement */ getSnapCanvas(): HTMLCanvasElement { return this.cutPic2Canvas(this.video, this.canvas); } /** * 截图 返回 img * @returns HTMLImageElement */ getSnapBase64(): Promise { return Promise.resolve(this.getSnapCanvas().toDataURL(IMAGE_TYPE.PNG, 1)); } /** * 截图 返回 img * @returns HTMLImageElement */ getSnapImg(): Promise { return this.getImgEle(this.getSnapCanvas().toDataURL(IMAGE_TYPE.PNG, 1)); } /** * 关闭视频 */ close() { return new Promise((resolve, reject) => { if (this.mediaStream?.active == true) { const track = this.mediaStream.getTracks()[0]; track.stop(); resolve(); this.isPlaying = false; return; } console.warn("this.mediaStream?.active: " + this.mediaStream?.active); this.isPlaying = false; // reject(new Error("this.mediaStream?.active: " + this.mediaStream?.active)); }); } /** * 截图 video 当前帧到 canvas * @param video * @param canvas */ cutPic2Canvas(video: HTMLVideoElement, canvas: HTMLCanvasElement = this.canvas) { const _canvas = canvas || document.createElement("canvas"); if (!video) { return; } let { width, height } = this.getDomWH(video); this.setDomWH(_canvas, width, height); const ctx = _canvas.getContext("2d"); ctx && ctx.drawImage(video, 0, 0, width, height, 0, 0, width, height); return _canvas; } /** * 截图 video 当前帧到 canvas * * 背景与用途: * 当视频原始分辨率(videoWidth/videoHeight)与显示尺寸(CSS宽高)不一致时, * 直接绘制会导致画面拉伸或变形。此方法采用“裁剪居中”策略: * @param video * @param canvas */ cutPic2CanvasFixSize(video: HTMLVideoElement = this.video, canvas: HTMLCanvasElement = this.canvas) { const _canvas = canvas || document.createElement("canvas"); if (!video) { return; } let { width: targetWidth, height: targetHeight } = this.getDomWH(video); // 视频原始尺寸 const srcW = video.videoWidth; const srcH = video.videoHeight; // 计算缩放比:取较大倍数,保证画布被完全覆盖(Cover 逻辑) const scaleX = targetWidth / srcW; const scaleY = targetHeight / srcH; // 取大值,多出的部分裁剪掉 const scale = Math.max(scaleX, scaleY); // 计算裁剪后的源图尺寸(从视频中截取的区域) const cropW = targetWidth / scale; const cropH = targetHeight / scale; // 计算裁剪起始点(居中) const cropX = (srcW - cropW) / 2; const cropY = (srcH - cropH) / 2; // 创建画布 _canvas.width = targetWidth; _canvas.height = targetHeight; const ctx = _canvas.getContext("2d"); // 清空画布 ctx.clearRect(0, 0, targetWidth, targetHeight); // 从视频中截取中间区域,绘制到画布(无旋转) ctx.drawImage( video, cropX, cropY, // 源图裁剪起始坐标 cropW, cropH, // 源图裁剪尺寸 0, 0, // 画布绘制起始坐标 targetWidth, targetHeight, // 画布绘制尺寸(完全填满) ); return _canvas; } /** * 获取video 宽高 * @param video */ getVideoSize() { const comStyle = window.getComputedStyle(this.video); return { width: parseInt(comStyle.width), height: parseInt(comStyle.height), }; } } export default CameraPreviewVideo;