使用 Vue3 实现摄像头拍照功能

发布于:2024-12-20 ⋅ 阅读:(119) ⋅ 点赞:(0)

参考资料: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>

代码功能总结

  1. 摄像头访问与视频流显示

    • 通过 navigator.mediaDevices.getUserMedia 请求用户的摄像头权限。
    • 成功后,将获取到的 MediaStream 赋值给 <video> 元素的 srcObject,实时显示摄像头捕获的视频流。
  2. 拍摄照片

    • 用户点击拍摄按钮时,触发 shoot 方法。
    • 方法内部创建一个 <canvas> 元素,将当前视频帧绘制到 canvas 上。
    • canvas 内容转换为 Base64 编码的 Data URL,并推入 images 数组中。
  3. 图片展示与数量限制

    • images 数组存储所有拍摄的照片,通过 v-for 循环在页面上显示。
    • 使用计数器 count 限制最多拍摄 10 张照片,防止内存占用过高。
  4. 界面样式与布局

    • 使用 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>

效果图 


网站公告

今日签到

点亮在社区的每一天
去签到