参考资料:MediaDevices.getUserMedia() - Web API | MDN
重要: navigator.mediaDevices.getUserMedia
需要在安全的上下文中运行。现代浏览器要求摄像头和麦克风的访问必须通过 HTTPS 或 localhost
(被视为安全的本地环境)进行,如果上传服务器地址是http开头的,而不是https,可以使用免费的Let's Encrypt打开,例子:进入宝塔-点击网络-点击自己的的链接-点击ssl-选择Let's Encrypt配置就好
模板部分
-
<video>
标签:- 作用:用于显示摄像头捕获的视频流。
- 属性:
ref="videoRef"
:通过ref
获取该 DOM 元素的引用,以便在脚本中操作。autoplay
:自动播放视频流,无需用户手动点击播放。playsinline
:在移动设备上防止视频自动全屏播放,保持在页面内嵌显示。
-
拍摄按钮:
- 使用一个
<div>
元素作为按钮,并绑定@click="shoot"
事件,当用户点击按钮时触发shoot
方法。 - 内部嵌套一个小的
<div class="shoot_buttons"></div>
,用于实现按钮的视觉效果
- 使用一个
-
显示捕获的图片:
- 使用一个
<div class="images">
容器包裹所有捕获的图片。 - 通过
v-for="(img, index) in images"
指令遍历images
数组,为每个图片生成一个<img>
元素。 :key="index"
:为每个循环生成的元素提供唯一的键值,优化渲染性能。:src="img"
:绑定图片的来源为数组中的每个 Data URL。
- 使用一个
<template>
<div class="container">
<!-- 视频元素,用于显示摄像头捕获的视频流 -->
<video ref="videoRef" autoplay playsinline></video>
<!-- 拍摄按钮 -->
<div @click="shoot" class="shoot_button">
<div class="shoot_buttons"></div>
</div>
<!-- 循环显示所有捕获的图片 -->
<div class="images">
<img v-for="(img, index) in images" :key="index" :src="img" />
</div>
</div>
</template>
js部分
-
引入 Vue 的响应式 API:
ref
:用于创建响应式的数据引用。onMounted
:生命周期钩子,在组件挂载完成后执行相应的操作。
-
获取视频 DOM 元素的引用:
const videoRef = ref(null);
:通过ref
获取<video>
元素,以便在脚本中操作其srcObject
属性,显示视频流。
-
定义存储图片的数组:
const images = ref([]);
:创建一个响应式数组,用于存储所有拍摄的图片的 Data URL。
-
计数器限制拍摄数量:
let count = 0;
:定义一个计数器,限制用户最多只能拍摄 10 张照片。- 在
shoot
方法中,每拍摄一张照片,计数器加一,当计数器超过 9 时,弹出警告并停止拍摄。
-
启动摄像头:
startCamera
函数使用navigator.mediaDevices.getUserMedia
请求用户的摄像头权限。- 请求成功后,将获取到的
MediaStream
赋值给视频元素的srcObject
,从而在页面上显示视频流。 - 捕获异常并在控制台输出错误信息,以便调试和提示用户。
-
组件挂载后启动摄像头:
onMounted
钩子确保在组件挂载完成后立即调用startCamera
,启动摄像头。
-
实现拍摄功能:
shoot
方法在用户点击拍摄按钮时触发。- 检查拍摄次数是否超过限制,若超过则弹出警告并返回。
- 获取当前视频流的帧,通过创建一个
<canvas>
元素,将视频帧绘制到canvas
上。 - 使用
canvas.toDataURL("image/png")
将canvas
内容转换为 Base64 编码的图片 URL。 - 将生成的 Data URL 推入
images
数组,触发页面上图片的循环显示。
<script setup>
import { ref, onMounted } from "vue";
// 获取存储的DOM节点
const videoRef = ref(null);
// 定义 images 用于存储捕获的图片数组
const images = ref([]);
// 计数器,用于限制拍摄的图片数量
let count = 0;
// 定义一个函数来请求用户的摄像头权限并在成功后设置视频流
const startCamera = async () => {
try {
// 请求用户媒体(仅视频)
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
// 将获取到的 MediaStream 赋值给视频元素的 srcObject
videoRef.value.srcObject = stream;
} catch (error) {
console.error("无法访问摄像头", error);
}
};
// 当组件挂载完成后启动摄像头
onMounted(() => {
startCamera();
});
// 点击拍摄
const shoot = () => {
if (count > 9) {
alert("最多只能拍摄10张照片");
return;
}
count++;
const video = videoRef.value;
if (!video) return;
// 创建一个canvas元素
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// 在canvas上绘制当前的视频帧
const ctx = canvas.getContext("2d");
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 将canvas内容转换为Data URL并推入 images 数组
const dataURL = canvas.toDataURL("image/png");
images.value.push(dataURL);
};
</script>
样式部分
<style scoped>
/* 容器样式 */
.container {
font-family: Arial, sans-serif; /* 设置字体 */
text-align: center; /* 文本居中 */
margin-top: 50px; /* 顶部外边距 */
}
/* 视频元素样式 */
video {
width: 300px; /* 宽度 */
height: auto; /* 高度自动调整以保持视频的原始比例 */
background-color: #000; /* 背景颜色为黑色,防止视频加载前显示空白 */
border: 1px solid #ccc; /* 灰色边框 */
}
/* 拍摄按钮样式 */
.shoot_button {
margin-top: 10px;
width: 60px;
height: 60px;
border: 4px solid red;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
left: 50%;
transform: translateX(-50%);
cursor: pointer;
background-color: white;
}
.shoot_buttons {
width: 30px;
height: 30px;
background-color: red;
border-radius: 50%;
}
/* 图片容器样式 */
.images {
width: 850px;
height: auto;
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 20px;
border: 2px gray solid;
padding: 10px;
box-sizing: border-box;
position: fixed;
left: 50%;
transform: translateX(-50%);
bottom: 20px;
background-color: rgba(255, 255, 255, 0.8);
}
/* 图片样式 */
img {
width: 70px;
height: 80px;
margin: 10px;
border: 1px solid #ccc;
object-fit: cover;
}
</style>
代码功能总结
-
摄像头访问与视频流显示:
- 通过
navigator.mediaDevices.getUserMedia
请求用户的摄像头权限。 - 成功后,将获取到的
MediaStream
赋值给<video>
元素的srcObject
,实时显示摄像头捕获的视频流。
- 通过
-
拍摄照片:
- 用户点击拍摄按钮时,触发
shoot
方法。 - 方法内部创建一个
<canvas>
元素,将当前视频帧绘制到canvas
上。 - 将
canvas
内容转换为 Base64 编码的 Data URL,并推入images
数组中。
- 用户点击拍摄按钮时,触发
-
图片展示与数量限制:
images
数组存储所有拍摄的照片,通过v-for
循环在页面上显示。- 使用计数器
count
限制最多拍摄 10 张照片,防止内存占用过高。
-
界面样式与布局:
- 使用 CSS 样式美化视频、按钮和图片的展示。
- 确保界面在不同设备上有良好的用户体验,特别是在移动设备上的布局调整。
完整代码
<template>
<div class="container">
<!-- 视频元素,用于显示摄像头捕获的视频流 -->
<video ref="videoRef" autoplay playsinline></video>
<!-- 按钮 -->
<div @click="shoot" class="shoot_button">
<div class="shoot_buttons"></div>
</div>
<!-- 循环显示所有捕获的图片 -->
<div class="images">
<img v-for="(img, index) in images" :key="index" :src="img" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
// 获取存储的DOM节点
const videoRef = ref(null);
// 定义 images 用于存储捕获的图片数组
const images = ref([]);
// 计数
let count = 0;
// 定义一个函数来请求用户的摄像头权限并在成功后设置视频流
const startCamera = async () => {
try {
// 请求用户媒体(仅视频)
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
videoRef.value.srcObject = stream;
} catch (error) {
console.error("无法访问摄像头", error);
}
};
// 当组件挂载完成后启动摄像头
onMounted(() => {
startCamera();
});
// 点击拍摄
const shoot = () => {
if (count > 9) {
alert("最多只能拍摄10张照片");
return;
}
count++;
const video = videoRef.value;
if (!video) return;
// 创建一个canvas元素
const canvas = document.createElement("canvas");
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// 在canvas上绘制当前的视频帧
const ctx = canvas.getContext("2d");
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
// 将canvas内容转换为Data URL并推入 images 数组
const dataURL = canvas.toDataURL("image/png");
images.value.push(dataURL);
};
</script>
<style scoped>
/* 容器样式 */
.container {
font-family: Arial, sans-serif; /* 设置字体 */
text-align: center; /* 文本居中 */
margin-top: 50px; /* 顶部外边距 */
}
/* 视频元素样式 */
video {
width: 300px; /* 宽度 */
height: auto; /* 高度自动调整以保持视频的原始比例 */
background-color: #000; /* 背景颜色为黑色,防止视频加载前显示空白 */
border: 1px solid #ccc; /* 灰色边框 */
}
/* 按钮样式 */
.shoot_button {
margin-top: 10px;
width: 30px;
height: 30px;
border: 2px solid red;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
position: fixed;
left: 50%;
transform: translateX(-50%);
}
.shoot_buttons{
width: 20px;
height: 20px;
background-color: red;
border-radius: 50%;
}
/* 图片容器样式 */
.images {
width: 850px;
height: 100px;
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 200px;
border: 2px gray solid;
position: fixed;
left: 50%;
transform: translateX(-50%);
}
/* 图片样式 */
img {
width: 70px;
height: 80px;
margin-right: 10px;
margin-top: 10px;
}
</style>