WEB 端拍照组件

一、实现原理

这个组件是 web 端的组件,使用的也是 webAPI,我们最主要的 API 是navigator,这个 API 给了我们调用媒体设备的能力。在我们通过 API 获取到媒体数据后我们可以通过转换的形式将得到的流数据用 video 展示出来。接下来我们会用到画布canvas,主要还是通过画布的drawImage方法来获取 video 标签的某一帧图片。

二、代码

1.html 部分:

<template>
  <el-dialog :title="title" :visible.sync="photoVisible" width="60%" :before-close="handleClose" custom-class="dialog-double">
    <div id="photo-wrapper">
      <div class="btn-group">
        <el-button class="changeButton" @click="getCompetence" type="primary">开启摄像头</el-button>
        <el-button class="changeButton" @click="closeCamera" :disabled="closeAble && mediaStreamTrack">关闭摄像头</el-button>
        <el-button class="changeButton" id="btn" type="primary">拍照</el-button>
        <el-button class="changeButton" id="btn1" type="primary" @click="submit">提交图片</el-button>
      </div>
      <el-select v-if="devicesList.length > 1" v-model="currentDeviceID" clearable placeholder="请选择设备">
        <el-option v-for="device in devicesList" :key="device.deviceId" :label="device.label" :value="device.deviceId">
        </el-option>
      </el-select>
      <div class="show-box">
        <video src="" id="v1" :style="showStyle"></video>
        <canvas id="c1" width="800" height="450" :style="{ display: 'none' }"></canvas>
        <img id="img" src="" :style="showStyle" />
      </div>
    </div>
  </el-dialog>
</template>

2.script 部分

export default {
  name: "BrowserPhoto",
  props: {
    photoVisible: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      default: "",
    },
    showStyle: {
      type: Object,
      default: () => ({
        width: "400px",
      }),
    },
    videoConfig: {
      type: Object,
      default: () => ({
        width: 1280, //1280
        height: 720, //720
      }),
    },
    // 是否在没有摄像头开启时禁用关闭按钮
    closeAble: {
      type: Boolean,
      default: false,
    },
    url: {
      type: String,
    },
  },
  data() {
    return {
      currentDeviceID: "", //当前媒体设备ID
      mediaStreamTrack: "", //媒体设备对象
      devicesList: [], //设备列表
      imgFile: "", //图片文件
    };
  },
  watch: {
    currentDeviceID(newV, oldV) {
      console.log(newV);
      if (newV !== oldV) {
        this.closeCamera();
      }
    },
  },
  methods: {
    searchDevice() {
      if (window.navigator.mediaDevices && window.navigator.mediaDevices.getUserMedia) {
      }
      navigator.mediaDevices.enumerateDevices().then((devices) => {
        let videoList = devices.filter((item) => {
          return item.kind == "videoinput";
        });
        this.devicesList = videoList;
        if (videoList.length == 0) {
          this.$message({
            message: "没有查询到视频输入设备!",
            type: "error",
          });
          return false;
        } else if (videoList.length == 1) {
          this.currentDeviceID = videoList[0].deviceId;
        } else if (videoList.length > 1) {
          this.$message({
            message: "当前设备有多个视频输入设备,可根据需要切换视频输入设备",
            type: "info",
          });
        }
      });
    },
    getCompetence() {
      let video = document.querySelector("#v1");
      let photo = document.querySelector("#c1");
      let ctx = photo.getContext("2d");
      let btn = document.querySelector("#btn");
      let btn1 = document.querySelector("#btn1");
      let img = document.querySelector("#img");
      let download = document.querySelector("#download");
      const h = this.$createElement;
      navigator.mediaDevices
        .getUserMedia({
          video: {
            deviceId: this.currentDeviceID || this.devicesList[0],
            ...this.videoConfig,
          },
          audio: false,
        })
        .then((stream) => {
          let mediaStreamTrack = stream.getTracks()[0];
          this.mediaStreamTrack = mediaStreamTrack;
          video.srcObject = stream;
          video.play();
          btn.addEventListener(
            "click",
            () => {
              let devicePixelRatio = window.devicePixelRatio || 1;
              let backingStoreRatio =
                ctx.webkitBackingStorePixelRatio ||
                ctx.mozBackingStorePixelRatio ||
                ctx.msBackingStorePixelRatio ||
                ctx.oBackingStorePixelRatio ||
                ctx.backingStorePixelRatio ||
                1;
              let ratio = devicePixelRatio / backingStoreRatio;
              console.log(ratio);
              photo.width = photo.width * ratio;
              photo.height = photo.height * ratio;
              ctx.drawImage(
                video,
                0,
                0,
                800 * ratio,
                ((800 * this.videoConfig.height) / this.videoConfig.width) * ratio
              );
              ctx.scale(ratio, ratio);

              // canvas.toDataURL 返回的是一串Base64编码的URL
              img.src = photo.toDataURL("image/png");
              let arr = photo.toDataURL("image/png").split(",");
              let type = arr[0].match(/:(.*?);/)[1];
              let bstr = atob(arr[1]);
              let n = bstr.length;
              let u8arr = new Uint8Array(n);
              while (n--) {
                u8arr[n] = bstr.charCodeAt(n);
              }
              this.imgFile = new File([u8arr], "img" + new Date().getTime(), { type });
              console.log(this.imgFile);
              this.$emit("upload", this.imgFile);
            },
            false
          );
        })
        .catch((err) => {
          console.log(err);
        });
    },
    closeCamera() {
      console.log(this.mediaStreamTrack);
      if (this.mediaStreamTrack) {
        this.mediaStreamTrack.stop();
      }
      // else {
      //   this.$message({
      //     message: "关闭失败,请检查当前设备是否开启摄像头",
      //     type: "error",
      //   });
      // }
    },
    handleClose() {
      this.$emit("update:photoVisible", false);
    },
    submit() {
      let url = this.url;
      let param = new FormData();
      param.append("file", this.imgFile);
      let config = {
        headers: { "Content-Type": "multipart/form-data" },
      };
      http.post(url, param, config).then((res) => {
        console.log(res);
        this.$emit("uploadSuccess", res);
      });
    },
  },
  mounted() {
    this.searchDevice();
    // if(this.closeAble){
    // }
  },
  beforeDestroy() {
    this.closeCamera();
  },
};

三、注意事项

  1. 因为画布的限制,所以该方法在绘画成图片的时候画质会得到压缩,重点在于绘画的时候需要调整ratio参数的大小,这是因为画笔的默认像素大小限制会导致图片绘画的时候不够精细从而出现模糊的问题。
  2. 其次是图片的大小,img 标签的宽高也会影响到图片的质量。图片的宽高设定主要是根据摄像头的分辨率来设定的。除了宽高比图片的大小也会影响图片的质量,具体大小需要根据实际情况调整。

具体实例可参考天润项目的解决方案。