js通过浏览器调用电脑摄像头和麦克风进行视频录制

105 阅读2分钟

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

<template>
  <div>
    <el-dialog title="视频录制" :model-value="props.isVideoRecordDialogShow" width="870" :before-close="handleClose">
      <div v-loading="isDetecting">
        <div class="video-area" v-loading="isLoading || isDetecting" :element-loading-text="isLoading ? '正在启用摄像头' : '正在进行人脸检测'">
          <div class="left-content">
            <video ref="videoElement" class="left-video"></video>
            <div class="video-timer-area" v-if="!recordedVideoURL && recording">
              <span class="red-dot"></span>
              <div class="timer-text"> {{ `${h}:${m}:${s}` }}</div>
            </div>
            <div class="center">
              <el-button type="primary" style="width: 140px" @click="startRecording" :disabled="isLoading">{{ btntext }}</el-button>
            </div>
          </div>
          <div class="right-content">
            <div v-if="recordedVideoURL && !recording">
              <video controls class="right-video">
                <source :src="recordedVideoURL" type="video/mp4" />
              </video>
            </div>
            <div v-if="!recordedVideoURL || recording">
              <el-empty class="right-empty" description="请先在左侧录制视频哦~" :image="emptyImg" />
            </div>
            <div class="center">
              <el-button type="primary" style="width: 140px" :disabled="recording" @click="toUse">使用</el-button>
            </div>
          </div>
        </div>
      </div>
    </el-dialog>
  </div>
</template>

<script setup>
import { computed, ref, onMounted, watch } from 'vue';
import { ElDialog, ElButton, ElMessage, vLoading } from 'element-plus';
import { DetectFace } from '@/api/index.js';
import emptyImg from '@/assets/icons/svg/video.svg';

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

let btntext = computed(() => (recording.value ? '停止录制' : '开始录制'));
let isLoading = ref(false);

const emit = defineEmits(['closeVideoRecordDialog', 'setRecordVideo']);
const handleClose = () => {
  // 释放摄像头
  videoStream.value && videoStream.value.getTracks().forEach((track) => track.stop());
  emit('closeVideoRecordDialog');
};

const videoStream = ref(null);
const mediaRecorder = ref(null);
const recordedChunks = ref([]);
const recordedVideoURL = ref(null);
const recording = ref(false);
const snapshotURL = ref(null);
const videoElement = ref(null);
const canvasElement = ref(null);
const recorderFile = ref(null);

onMounted(async () => {
  isLoading.value = true;
  videoStream.value = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
  videoElement.value.srcObject = videoStream.value;
  // 避免回音
  videoElement.value.volume = 0;
  videoElement.value.play();
  isLoading.value = false;
});

const startRecording = async () => {
  recordedVideoURL.value = null;
  try {
    // 如果已经在录制中,就停止录制
    if (mediaRecorder.value && recording.value) {
      stopRecording();
      return;
    }
    // 开始计时
    startTimer();
    mediaRecorder.value = new MediaRecorder(videoStream.value);
    recordedChunks.value = [];
    mediaRecorder.value.ondataavailable = (event) => {
      if (event.data.size > 0) {
        recordedChunks.value.push(event.data);
      }
    };
    mediaRecorder.value.onstop = () => {
      recorderFile.value = new Blob(recordedChunks.value, { type: 'video/mp4' });
      recordedVideoURL.value = URL.createObjectURL(recorderFile.value);
      // 结束计时
      stopTimer();
    };

    mediaRecorder.value.start();
    recording.value = true;
  } catch (error) {
    // console.error('Error accessing media devices: ', error);
  }
};

const stopRecording = () => {
  if (mediaRecorder.value && recording.value) {
    mediaRecorder.value.stop();
    // videoStream.value.getTracks().forEach(track => track.stop());
    recording.value = false;
  }
};

let isDetecting = ref(false);
const toUse = () => {
  if (!recordedVideoURL.value) {
    ElMessage.warning('请先录制视频');
    return;
  }
  let files = new File([recorderFile.value], 'recordedVideo.mp4', { type: 'video/mp4' });
  // 对视频进行人脸检测
  const formData = new FormData();
  formData.append('video', files);
  isDetecting.value = true;
  DetectFace(formData)
    .then((res) => {
      // 人脸检测通过,可以进行使用
      emit('setRecordVideo', files, recordedVideoURL.value);
      handleClose();
    })
    .catch((e) => {
      // ElMessage.error('视频中未检测到人脸,请重新录制');
    })
    .finally(() => {
      isDetecting.value = false;
    });
};

// 定义一个计时器, 以hh:mm:ss格式显示时间
let timer = ref(null);
let h = ref('00');
let m = ref('00');
let s = ref('00');
const startTimer = () => {
  let time = 0;
  timer.value = setInterval(() => {
    time++;
    h.value = Math.floor(time / 3600)
      .toString()
      .padStart(2, '0');
    m.value = Math.floor((time % 3600) / 60)
      .toString()
      .padStart(2, '0');
    s.value = (time % 60).toString().padStart(2, '0');
  }, 1000);
};
// 停止计时
const stopTimer = () => {
  clearInterval(timer.value);
  h.value = '00';
  m.value = '00';
  s.value = '00';
};

// 当录制时长超过5s时,自动停止录制
watch(
  () => s.value,
  (newV) => {
    if (newV === '20') {
      ElMessage.warning('录制时长到达20s,自动停止录制');
      stopRecording();
    }
  },
  { immediate: true },
);
</script>

<style lang="less" scoped>
.show-record {
  visibility: visible;
}
.hidden-record {
  visibility: hidden;
}
.video-area {
  display: flex;
  justify-content: center;
  align-items: center;
  .left-content {
    position: relative;
  }
  .right-content {
    margin-left: 20px;
  }
}
.left-video {
  width: 400px;
  height: 300px;
  border-radius: @borderRadius;
  margin-bottom: 10px;
}

.right-video {
  width: 400px;
  height: 300px;
  border-radius: @borderRadius;
  margin-bottom: 10px;
}

.right-empty {
  width: 400px;
  height: 300px;
  margin-bottom: 15px;
  background-color: #e2eefa;
  border-radius: @borderRadius;
}

.video-timer-area {
  position: absolute;
  top: 10px;
  right: 10px;
  .timer-text {
    width: 70px;
    display: inline-block;
  }
}
</style>