废话不多说直接上代码,代码解析看下文。
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import { setting } from '@/common/setting';
import { useQuestionStore } from '@/stores/modules/examStore.js';
// 试题store
const questionStore = useQuestionStore();export function useCameraCapture({ time = 5000, photoNum = 5 }) {const videoSrc = ref('');const imgUrl = ref('');const video = ref(null);const fileUploadToken = ref('');const picList = ref([]);const attIds = ref([]);let captureInterval = null;const connectCamera = () => {try {navigator.mediaDevices.getUserMedia({video: true,}).then(stream => {video.value.srcObject = stream;video.value.onloadedmetadata = () => {video.value.play();// setTimeout(() => {// handleCapture();// }, 1000);};}).catch(err => {console.error('获取设备失败:', err);});} catch (error) {console.error('获取设备失败:', error);}};const handleCapture = async () => {await nextTick();// 拍照次数达到上限,清除定时器if (picList.value.length >= photoNum) {clearInterval(captureInterval);return;}const videoElement = document.querySelector('video'); // 获取 video 节点if (!videoElement || !(videoElement instanceof HTMLVideoElement)) {console.error('Video element not found or not an HTMLVideoElement');return;}const canvas = document.createElement('canvas'); // 创建 canvas 节点const w = videoElement.clientWidth;const h = videoElement.clientHeight;canvas.width = w; // 设置宽高canvas.height = h; // 设置宽高const ctx = canvas.getContext('2d');if (!ctx) {console.error('Canvas context not found');return;}ctx.drawImage(videoElement, 0, 0, w, h); // video 写入到 canvasimgUrl.value = canvas.toDataURL('image/png'); // 生成截图uni.uploadFile({url: `/api${setting.fileUrl}`, // 服务器上传接口地址filePath: imgUrl.value,name: 'file', // 必须填写,后台用来接收文件header: {'Blade-Auth': fileUploadToken.value,'Blade-Requested-With': 'BladeHttpRequest',},formData: {user: 'test', // 其他要传的参数},success: uploadFileRes => {let picData = JSON.parse(uploadFileRes.data);if (picList.value.length >= photoNum) {return;} else {let str = {};str.attachId = picData.data.attachId;str.src = picData.data.link;// picList.value.push(str);attIds.value.push(picData.data.attachId);// 将attIds存储到store中questionStore.setAttIds(attIds.value);// 从store中获取attIdspicList.value = questionStore.getAttIds}},fail: uploadFileErr => {console.log('uploadFileErr', uploadFileErr);},});};onMounted(async () => {// 获取token(附件上传所需的token)const token = uni.getStorageSync('accessToken');fileUploadToken.value = `bearer ${token}`;await nextTick();connectCamera();captureInterval = setInterval(handleCapture, time); // 每隔5秒调用一次handleCapture});onUnmounted(() => {if (captureInterval) {clearInterval(captureInterval); // 清除定时器}});return {videoSrc,imgUrl,video,fileUploadToken,picList,attIds,time,handleCapture,};
}
这段代码是一个 Vue 3 的自定义 Hook,用于实现摄像头捕获功能,并将捕获的图像上传到服务器。以下是对这段代码的详细讲解:
导入依赖
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import { setting } from '@/common/setting';
import { useQuestionStore } from '@/stores/modules/examStore.js';
ref
、onMounted
、onUnmounted
和nextTick
是 Vue 3 的组合式 API,用于管理响应式数据和生命周期钩子。setting
是一个配置文件,包含了文件上传的 URL。useQuestionStore
是一个 Pinia store,用于管理试题相关的数据。
定义 store 实例
const questionStore = useQuestionStore();
- 创建一个
questionStore
实例,用于在 Hook 中访问和修改 store 中的数据。
定义 Hook
export function useCameraCapture({ time = 5000, photoNum = 5 }) {
- 定义一个名为
useCameraCapture
的函数,接收一个包含time
和photoNum
的对象作为参数。time
:拍照间隔时间,默认为 5000 毫秒(5 秒)。photoNum
:拍照次数上限,默认为 5 次。
定义响应式数据
const videoSrc = ref('');
const imgUrl = ref('');
const video = ref(null);
const fileUploadToken = ref('');
const picList = ref([]);
const attIds = ref([]);
let captureInterval = null;
videoSrc
:视频源 URL。imgUrl
:捕获的图像 URL。video
:视频元素的引用。fileUploadToken
:文件上传的令牌。picList
:捕获的图像列表。attIds
:附件 ID 列表。captureInterval
:定时器 ID,用于控制拍照间隔。
连接摄像头
const connectCamera = () => {try {navigator.mediaDevices.getUserMedia({video: true,}).then(stream => {video.value.srcObject = stream;video.value.onloadedmetadata = () => {video.value.play();};}).catch(err => {console.error('获取设备失败:', err);});} catch (error) {console.error('获取设备失败:', error);}
};
- 使用
navigator.mediaDevices.getUserMedia
获取摄像头视频流,并将其设置为视频元素的源。 - 在视频元数据加载完成后,开始播放视频。
捕获图像
const handleCapture = async () => {await nextTick();if (picList.value.length >= photoNum) {clearInterval(captureInterval);return;}const videoElement = document.querySelector('video');if (!videoElement || !(videoElement instanceof HTMLVideoElement)) {console.error('Video element not found or not an HTMLVideoElement');return;}const canvas = document.createElement('canvas');const w = videoElement.clientWidth;const h = videoElement.clientHeight;canvas.width = w;canvas.height = h;const ctx = canvas.getContext('2d');if (!ctx) {console.error('Canvas context not found');return;}ctx.drawImage(videoElement, 0, 0, w, h);imgUrl.value = canvas.toDataURL('image/png');uni.uploadFile({url: `/api${setting.fileUrl}`,filePath: imgUrl.value,name: 'file',header: {'Blade-Auth': fileUploadToken.value,'Blade-Requested-With': 'BladeHttpRequest',},formData: {user: 'test',},success: uploadFileRes => {let picData = JSON.parse(uploadFileRes.data);if (picList.value.length >= photoNum) {return;} else {let str = {};str.attachId = picData.data.attachId;str.src = picData.data.link;attIds.value.push(picData.data.attachId);questionStore.setAttIds(attIds.value);picList.value = questionStore.getAttIds;}},fail: uploadFileErr => {console.log('uploadFileErr', uploadFileErr);},});
};
- 使用
nextTick
确保 DOM 更新完成。 - 检查拍照次数是否达到上限,如果达到则清除定时器并返回。
- 获取视频元素,并将视频帧绘制到 Canvas 上。
- 将 Canvas 转换为图像 URL,并上传到服务器。
- 在上传成功后,将附件 ID 和图像 URL 存储到
picList
和attIds
中,并更新 store。
生命周期钩子
onMounted(async () => {const token = uni.getStorageSync('accessToken');fileUploadToken.value = `bearer ${token}`;await nextTick();connectCamera();captureInterval = setInterval(handleCapture, time);
});onUnmounted(() => {if (captureInterval) {clearInterval(captureInterval);}
});
- 在组件挂载时,获取上传令牌,连接摄像头,并设置定时器定期调用
handleCapture
。 - 在组件卸载时,清除定时器。
返回值
return {videoSrc,imgUrl,video,fileUploadToken,picList,attIds,time,handleCapture,
};
- 返回响应式数据和方法,以便在组件中使用。
总结
这个 Hook 实现了以下功能:
- 连接摄像头并获取视频流。
- 定期捕获视频帧并生成图像。
- 将生成的图像上传到服务器。
- 将上传的附件 ID 和图像 URL 存储到 Pinia store 中。
- 提供响应式数据和方法,以便在组件中使用。