自定义vue摄像头 自定义时长,自定义大小

javascript 复制代码
<template>
  <div style="height: 100vh;background: #000;">
    <div style="position: fixed;top: 15px;left: 15px;color: #fff;font-size: 18px;z-index: 100;" @click="$router.go(-1)">
      <img src="./img/left.svg" alt="" style="width: 25px;height: 25px;">
    </div>
    <div v-if="recordedVideoUrl" style="position: fixed;top: 15px;right: 15px;color: #fff;font-size: 18px;z-index: 100;" @click="getto">
      <img src="./img/dh.svg" alt="" style="width: 30px;height: 30px;">
    </div>
    <video ref="video" autoplay muted playsinline></video>
    <div class="video_btn" style="bottom: 130px;" v-if="!isRecording && deviceslistnum > 0 && !currentText">
        <div class="video_btn_item">
          <div :class="tabindex == id ? 'video_btn_item_topa' : 'video_btn_item_top'"  v-for="(it,id) in deviceslistnum" :key="id" @click="setdevices(id)">
              {{ '后置' + (id + 1) }}
          </div>
        </div>
    </div>

    <div class="video_btn" v-if="!recordedVideoUrl">
      <div style="width: 30px;height: 30px;">
        <!-- 手电筒控制按钮 -->
        <!-- <img v-if="isFlashSupported && !currentText && !isRecording" :src="isTorchOn ? './img/sdtk.svg' : './img/sdtg.svg'" alt="手电筒" @click="toggleTorch" style="width: 30px;height: 30px;"/> -->
      </div>
      <div class="btn_round">
        <div class="progress-button" @touchstart.prevent="gtouchstart" @touchend.prevent="gtouchend">
          <svg class="progress-circle" width="90" height="90">
            <circle class="progress-circle-background" cx="45" cy="45" r="35" />
            <circle class="progress-circle-fill" cx="45" cy="45" r="35" :style="circleStyle" />
          </svg>
          <div class="progress-text"></div>
        </div>
      </div>
      <div style="width: 30px;height: 30px;">
        <img v-if="!isRecording" src="./img/tanform.svg" alt="" @click="setdevicestwo" style="width: 30px;height: 30px;">
      </div>
    </div>
    <div  class="video_btn" v-else>
      <div class="video_btn_bottom" style="color: #fff;" @click="clearvideo">
         重新录制
      </div>
    </div>
  </div>
</template>

<script>
import { mapState } from "vuex"
export default {
  data() {
    return {
      mediaStream: null, // 摄像头视频流
      mediaRecorder: null, // 媒体录制器
      recordedChunks: [], // 录制的数据块
      recordedVideoUrl: null, // 录制后的视频 URL
      isRecording: false, // 是否正在录制
      currentDeviceId: null, // 当前摄像头设备 ID
      currentText: true, // true前 false后
      devices: [], // 设备列表
      deviceslistnum: 0,
      timer: null, // 计时器,用于限制录制时间
      timernum: 30, // 计时器,用于限制录制时间
      maxFileSize: 100 * 1024 * 1024, // 100MB


      progress: 0,
      interval: null,

      isTorchOn: false, // 手电筒状态
      isFlashSupported: false, // 是否支持闪光灯

      tabindex: 0,
    }
  },
  computed: {
    ...mapState("login", ["token", "giant_opo_userdata"]),
    circleStyle() {
      const circumference = 2 * Math.PI * 40
      const offset = circumference - (this.progress / 100) * circumference
      return {
        strokeDasharray: `${circumference} ${circumference}`,
        strokeDashoffset: offset,
      }
    },
  },
  mounted() {
    this.startCamera()
  },
  methods: {
    // 切换摄像头
    setdevices(val) { 
      this.tabindex = val
      this.switchCamera(val + 1)
    },
    // 
    setdevicestwo() { 
      if (this.currentText) {
        this.switchCamera(1)
        this.currentText = false
      } else {
        this.switchCamera(0)
        this.currentText = true
      }
    },
    gtouchstart() {
      // //真正长按后应该执行的内容
      this.isRecording = true
      this.startProgress()
      this.startRecording()
      console.log('按下')
      return false
    },

    //手释放,如果在500毫秒内就释放,则取消长按事件,此时可以执行onclick应该执行的事件
    gtouchend() {
      this.isRecording = false
      this.progress = 0
      clearInterval(this.interval)
      this.stopRecording()
      console.log('松开')
      return false
    },

    startProgress() {
      this.interval = setInterval(() => {
        if (this.progress >= 100) {
          clearInterval(this.interval)
          this.interval = null
          return
        }
        this.progress += 1
      }, 300)
    },
    async startCamera() {
      try {
        // 获取设备列表
        const devices = await navigator.mediaDevices.enumerateDevices()
        this.devices = devices.filter(device => device.kind === "videoinput")

        if (this.devices.length === 0) {
          throw new Error("没有找到摄像头设备")
        }

        // 默认选择第一个设备 首次前置
        this.currentDeviceId = this.devices[0].deviceId

        // 获取视频流并绑定
        this.mediaStream = await navigator.mediaDevices.getUserMedia({
          video: { deviceId: this.currentDeviceId }
        })

        const videoElement = this.$refs.video
        videoElement.srcObject = this.mediaStream


        // 检查设备是否支持闪光灯
        const videoTrack = this.mediaStream.getVideoTracks()[0]
        if (videoTrack && "torch" in videoTrack.getCapabilities()) {
          this.isFlashSupported = true
        }

      } catch (error) {
        console.error("无法访问摄像头:", error)
      }
    },
    // 控制闪光灯(手电筒)
    async toggleTorch() {
      if (this.isFlashSupported) {
        const videoTrack = this.mediaStream.getVideoTracks()[0]
        if (videoTrack) {
          try {
            // 切换 torch 状态
            const capabilities = videoTrack.getCapabilities()
            if (capabilities.torch) {
              this.isTorchOn = !this.isTorchOn
              await videoTrack.applyConstraints({
                advanced: [{ torch: this.isTorchOn }]
              })
            }
          } catch (error) {
            console.error("无法切换手电筒:", error)
          }
        }
      }
    },
    // 开始录制或停止录制
    startRecording() {
      if (this.mediaStream) {
        this.mediaRecorder = new MediaRecorder(this.mediaStream)
        this.recordedChunks = []
        this.mediaRecorder.ondataavailable = (event) => {
          if (event.data.size > 0) {
            this.recordedChunks.push(event.data)
            // 检查文件大小是否超过 95MB
            const totalSize = this.recordedChunks.reduce((acc, chunk) => acc + chunk.size, 0)
            if (totalSize > this.maxFileSize) {
              this.stopRecording()
            }
          }
        }
        this.mediaRecorder.onstop = () => {
          const blob = new Blob(this.recordedChunks, { type: "video/webm" })
          // this.recordedVideoUrl = URL.createObjectURL(blob)
          this.recordedVideoUrl = blob
        }
        this.timer = setInterval(() => {
          this.settime()
        }, 1000) // 60秒后停止录制
        this.mediaRecorder.start()
        this.isRecording = true
      }
    },
    settime() {
      if (this.timernum < 1) {
        this.stopRecording()
        clearInterval(this.timer)
        return
      } else {
        this.timernum--
      }
    },
    stopRecording() {
      if (this.mediaRecorder) {
        this.mediaRecorder.stop()
        clearInterval(this.timer) // 停止定时器
        this.isRecording = false
      }
      this.progress = 0
      this.timernum = 30
    },
    async switchCamera(val) {
      try {
        // 停止当前的媒体流
        this.stopStream()
        // 切换到下一个摄像头设备(循环切换)
        const devices = await navigator.mediaDevices.enumerateDevices()
        this.devices = devices.filter(device => device.kind === "videoinput")
        this.deviceslistnum = this.devices.length - 1
        // let currentIndex = 0
        // if (this.devices.length == 2) {
        // currentIndex = this.devices.findIndex(device => device.deviceId === this.currentDeviceId)
        // const nextIndex = (currentIndex + 1) % this.devices.length
        // this.currentDeviceId = this.devices[nextIndex].deviceId
        this.currentDeviceId = this.devices[val].deviceId
        // } else if (this.devices.length > 2) {
        //   if (this.currentText) {
        //     this.currentDeviceId = this.devices[1].deviceId
        //     this.currentText = false
        //   } else {
        //     this.currentDeviceId = this.devices[0].deviceId
        //     this.currentText = true
        //   }
        // }
        // 获取新的摄像头流
        this.mediaStream = await navigator.mediaDevices.getUserMedia({
          video: { deviceId: this.currentDeviceId }
        })

        const videoElement = this.$refs.video
        videoElement.srcObject = this.mediaStream
      } catch (error) {
        this.$toast('无法切换摄像头' + error)
      }
    },
    stopStream() {
      if (this.mediaStream) {
        const tracks = this.mediaStream.getTracks()
        tracks.forEach(track => track.stop())
      }
    },
    // 重新录制
    clearvideo() {
      this.isRecording = false
      this.progress = 0
      this.recordedVideoUrl = null
      clearInterval(this.interval)
    },
    // 发布
    getto() {
      let _this = this
      // let formdata = new FormData()
      // formdata.append("video", file)
      _this.$loadingService.show({}, true)
      let httpURL = _this.$store.state.api.qywx_admin_api + "/upload/video_blob"
      // console.log(file,'123123123123')
      _this.axios
        .post(httpURL, this.recordedVideoUrl, {
          headers: {
            token: _this.token,
          },
        })
        .then(function (res) {
          _this.$loadingService.hide()
          if (res.status == 200) {
            if (typeof res.data != "object") {
              try {
                res.data = JSON.parse(res.data)
              } catch (error) {
                _this.$toast("拉取数据失败:接口异常")
                console.error(error)
                return false
              }
            }
            if (res.data.status == 1) {
              if (_this.$route.query.path == 'addFriend') { 
                _this.$router.push({
                  path: '/' + _this.$route.query.path,
                  query: {
                    fileUrl: res.data.data.path
                  }
                })
              }
            } else {
              _this.$toast(res.data.msg || "出现了一些问题")
            }
          } else {
            _this.$toast(res.data.msg ? res.data.msg : "请求失败")
          }
        })
        .catch(function (error) {
          _this.$loadingService.hide()
          console.log(error)
          // file.status = "failed";
          // file.message = "上传失败";
          _this.$toast("接口异常,出现了一些问题")
        })
    }
  }
}
</script>

<style scoped lang='less'>
.video_btn {
  position: fixed;
  bottom: 30px;
  z-index: 99;
  display: flex;
  justify-content: space-around;
  align-items: center;
  width: 100%;
  max-width: 640px;
  margin: 0 auto;

  .video_btn_bottom{
    padding: 10px;
    text-align: center;
    background: #858585;
    border-radius: 10px;
  }
}

.video_btn_item{
  border-radius: 20px;
  display: flex;
}
.video_btn_item_topa{
  color: #fff;
  padding:0px 8px;
  border-radius: 20px;
  border: 1px solid #fff;
}

.video_btn_item_top{
  color: #fff;
  padding:0px 8px;
}

video {
  width: 100%;
  height: 80vh;
  max-width: 640px;
  margin: 0 auto;
}

/* 按钮 */
// .btn_round {
//   width: 60px;
//   height: 60px;
//   border-radius: 50px;
//   background: #fff;
//   position: relative;

//   .btn_round_item_one {
//     position: absolute;
//     width: 55px;
//     height: 55px;
//     border-radius: 50px;
//     background: #ccc;
//     z-index: 1;
//     top: 50%;
//     left: 50%;
//     transform: translate(-50%, -50%);
//   } 
//   .btn_round_item_two {
//     position: absolute;
//     width: 40px;
//     height: 40px;
//     border-radius: 50px;
//     background: #999898;
//     z-index: 2;
//     top: 50%;
//     left: 50%;
//     transform: translate(-50%, -50%);
//   }
// }


.progress-button {
  position: relative;
  cursor: pointer;
}

.progress-circle {
  transform: rotate(-90deg);
}

.progress-circle-background {
  fill: none;
  stroke: #fff;
  stroke-width: 5;
}

.progress-circle-fill {
  fill: none;
  stroke: #13e013;
  stroke-width: 5;
  transition: stroke-dashoffset 0.3s;
}

.progress-text {
  position: absolute;
  top: 48%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: #fff;
  width: 60px;
  height: 60px;
  border-radius: 60px;
}
</style>
相关推荐
bin915329 分钟前
DeepSeek与Vue.js组件开发:解锁AI与前端开发的融合密码
vue.js·deepseek
禁默35 分钟前
【学术投稿-第六届新材料与清洁能源国际学术会议(ICAMCE 2025)】组织与结构:HTML中的<fieldset>与<legend>标签解析
前端·html
南城巷陌36 分钟前
Node.js的API之dgram的用法详解
前端·node.js·dgram
不会&编程37 分钟前
第3章 使用 Vue 脚手架
前端·javascript·vue.js
ttod_qzstudio40 分钟前
基于Typescript,使用Vite构建融合Vue.js的Babylon.js开发环境
vue.js·typescript·babylon.js
杨晓风-linda42 分钟前
Angular-hello world
前端·javascript·angular.js
一只理智恩42 分钟前
Cesium 离线加载瓦片图
前端·javascript·arcgis
幸福右手牵1 小时前
WPS如何接入DeepSeek(通过JS宏调用)
javascript·人工智能·深度学习·wps·deepseek
115432031q1 小时前
基于SpringBoot养老院平台系统功能实现十一
java·前端·后端
AC-PEACE1 小时前
route 与 router 之间的差别
前端·网络