废话不多说直接上代码,代码解析看下文。
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 写入到 canvas
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;
// picList.value.push(str);
attIds.value.push(picData.data.attachId);
// 将attIds存储到store中
questionStore.setAttIds(attIds.value);
// 从store中获取attIds
picList.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
的对象作为参数。
定义响应式数据
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',
},
form
Data: {
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 中。
- 提供响应式数据和方法,以便在组件中使用。