js通过浏览器调用电脑摄像头进行拍照

172 阅读2分钟

代码基于vue3,chrome版本122.0.6261.131,未进行其他浏览器的兼容性验证。拷贝到工程文件中进行简单调整即可复用。

<template>
  <div>
    <el-dialog title="现场拍照" :model-value="props.isShowTakePhoto" width="900px" :before-close="handleClose">
      <div class="photo-content">
        <div class="left-content" v-loading="isOpening || isDetecting" :element-loading-text="isOpening ? '正在启用摄像头' : '正在进行人脸检测'">
          <video ref="videoElement" class="video-area"></video>
          <div class="photo-area">
            <canvas ref="canvasElement" style="display: none"></canvas>
            <div v-if="photoURL">
              <img :src="photoURL" alt="Photo" />
              <div class="img-bottom-text">当前照片</div>
            </div>
          </div>
          <SvgIcon name="take-photo-text" class="take-photo-text" @click="takePhoto" />
        </div>
      </div>
      <template #footer>
        <el-button type="primary" @click="toUse" :disabled="isCapturing" style="width: 140px">使用</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script setup>
import { ref, onMounted, watch } from 'vue';
import { ElDialog, ElButton, ElMessage, vLoading } from 'element-plus';
import { DetectFace } from '@/api/index.js';
import SvgIcon from '@/components/SvgIcon';

const videoElement = ref(null);
const canvasElement = ref(null);
const photoURL = ref(null);
const isCapturing = ref(false);
let mediaStream;
let isOpening = ref(false);

const props = defineProps({
  isShowTakePhoto: {
    type: Boolean,
    default: false,
  },
});

watch(
  () => props.isShowTakePhoto,
  (newV) => {
    if (!newV) {
      photoURL.value = null;
      closeCamera();
    }
  },
);

onMounted(async () => {
  try {
    isOpening.value = true;
    mediaStream = await navigator.mediaDevices.getUserMedia({ video: true });
    videoElement.value.srcObject = mediaStream;
    videoElement.value.play();
    isOpening.value = false;
  } catch (error) {
    // console.error('Error accessing media devices: ', error);
  }
});

const takePhoto = () => {
  isCapturing.value = true;
  const context = canvasElement.value.getContext('2d');
  canvasElement.value.width = videoElement.value.videoWidth;
  canvasElement.value.height = videoElement.value.videoHeight;

  context.drawImage(videoElement.value, 0, 0, canvasElement.value.width, canvasElement.value.height);
  photoURL.value = canvasElement.value.toDataURL('image/png');
  isCapturing.value = false;
};

const emit = defineEmits(['closeTakePhoto']);
const handleClose = () => {
  closeCamera();
  emit('closeTakePhoto');
};

const closeCamera = () => {
  // 释放摄像头
  mediaStream && mediaStream.getTracks().forEach((track) => track.stop());
};

let isDetecting = ref(false);
const toUse = () => {
  if (!photoURL.value) {
    ElMessage.warning('请先拍照');
    return;
  }
  let photoFile = dataURLtoBlob(photoURL.value);
  // 进行人脸检测
  const formData = new FormData();
  formData.append('image', photoFile);
  isDetecting.value = true;
  DetectFace(formData)
    .then((res) => {
      // 人脸检测成功
      emit('setPhoto', photoFile, photoURL.value);
      handleClose();
    })
    .catch(() => {
      // 未检测到人脸
    })
    .finally(() => {
      isDetecting.value = false;
    });
};

/**
 * Base64字符串转二进制流
 * @param {String} dataurl Base64字符串(字符串包含Data URI scheme,例如:data:image/png;base64, )
 */
const dataURLtoBlob = (dataurl) => {
  var arr = dataurl.split(','),
    mime = arr[0].match(/:(.*?);/)[1],
    bstr = atob(arr[1]),
    n = bstr.length,
    u8arr = new Uint8Array(n);
  while (n--) {
    u8arr[n] = bstr.charCodeAt(n);
  }
  return new Blob([u8arr], {
    type: mime,
  });
};

// return { videoElement, canvasElement, photoURL, takePhoto, isCapturing };
</script>

<style lang="less" scoped>
.photo-content {
  display: flex;
  justify-content: space-around;
  align-items: center;
  width: 100%;
  .left-content {
    position: relative;
  }
  .video-area {
    width: 500px;
    height: 375px;
    border-radius: @borderRadius;
  }
}
.photo-area {
  position: absolute;
  top: 0;
  right: -155px;
  img {
    width: 133px;
    height: 100px;
    border-top-right-radius: @borderRadius;
    border-top-left-radius: @borderRadius;
  }
}
.img-bottom-text {
  // width: 100px;
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: center;
  position: relative;
  top: -6px;
  background-color: #e2eefa;
  border-bottom-left-radius: @borderRadius;
  border-bottom-right-radius: @borderRadius;
}

.take-photo-text {
  width: 64px;
  height: 64px;
  color: var(--el-color-primary);
  position: absolute;
  left: 218px;
  bottom: 20px;
  cursor: pointer;
}
.take-photo-text:hover {
  box-shadow: 0px 0px 12px var(--el-color-primary);
  border-radius: 100px;
}
</style>