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

<template>
  <div style="height: 100vh;background: #000;">
    <div class="top_fixed" style="">
      <div @click="lastto" style="width: 30px;height: 30px;">
        <img src="./img/left.svg" alt="" style="width: 25px;height: 25px;">
      </div>
      <div style="font-size: 16px;"></div>
      <div style="width: 30px;height: 30px;">
        <template v-if="recordedVideoUrl">
          <img src="./img/dh.svg" alt="" style="width: 30px;height: 30px;" @click="getto">
        </template>
      </div>
    </div>
    <div style="background: #000;height: 80vh;">
      <video ref="video" autoplay muted playsinline autofocus
        style="width: 100%; height: 100%; object-fit: cover;"></video>
    </div>
    <div class="video_btn" v-if="!recordedVideoUrl">
      <div 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"
            :style="{ width: !isRecording ? '60px' : '30px', height: !isRecording ? '60px' : '30px' }">
          </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: 20, // 计时器,用于限制录制时间
      maxFileSize: 100 * 1024 * 1024, // 100MB
      progress: 0, // 按钮进度
      interval: null, // 按钮定时器存储

      // 新增摄像头支持状态
      cameraSupport: {
        hasFront: false,
        hasBack: false
      }
    }
  },
  beforeRouteEnter(to, from, next) {
    // console.log(111)
    next()
  },
  beforeRouteLeave(to, from, next) {
    // console.log(2222)
    next()
  },
  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,
      }
    },
  },
  async mounted() {
    const hasPermission = await this.checkCameraPermission()
    if (!hasPermission && this.$store.state.browser.ios) {
      // iOS 设备且未获取权限,需要用户点击激活
      // const handler = () => {
      //   this.initialCameraStart()
      //   document.removeEventListener('click', handler)
      // }
      // document.addEventListener('click', handler)
      // this.$toast('轻点屏幕启动摄像头')
      setTimeout(() => {
        this.initialCameraStart()
      }, 500)
    } else {
      // 已经获取权限,直接启动摄像头
      this.initialCameraStart()
    }

    // iOS 需要先触发点击事件
    // if (this.$store.state.browser.ios) {
    //   const handler = () => {
    //     this.initialCameraStart()
    //     document.removeEventListener('click', handler)
    //   }
    //   document.addEventListener('click', handler)
    //   this.$toast('轻点屏幕启动摄像头')
    // } else {
    //   this.initialCameraStart()
    // }
  },
  methods: {
    async checkCameraPermission() {
      try {
        const permissionStatus = await navigator.permissions.query({ name: 'camera' })
        if (permissionStatus.state === 'granted') {
          // 已经获取权限,直接启动摄像头
          this.initialCameraStart()
          return true
        }
        return false
      } catch (error) {
        console.error('权限检查失败:', error)
        return false
      }
    },
    async initialCameraStart() {
      if (this.mediaStream) {
        // 如果已经启动摄像头,直接返回
        return
      } else {
        try {

          
        // 暂时隐藏视频
        const videoElement = this.$refs.video
        videoElement.style.opacity = 0

        // 获取所有视频输入设备
        const devices = await navigator.mediaDevices.enumerateDevices()
        const videoDevices = devices.filter(device => device.kind === 'videoinput')

        let constraints = {
          video: {
            facingMode: { ideal: 'environment' },
            width: { ideal: 1280 },
            height: { ideal: 720 },
            frameRate: { ideal: 30 }
          }
        }
        // 对于安卓设备,如果是后置摄像头且有多个摄像头,则使用第二个摄像头
        if (!this.$store.state.browser.ios && videoDevices.length > 1) {
          constraints.video.deviceId = { exact: videoDevices[3].deviceId }
        } else if (this.$store.state.browser.ios && videoDevices.length > 1) {
          constraints.video.deviceId = { exact: videoDevices[2].deviceId }
        } 
        // 如果是后置摄像头,确保不使用变焦设置
        constraints.video.zoom = 1
        constraints.video.focusMode = 'continuous'

        this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints)
        videoElement.srcObject = this.mediaStream

        // 等待摄像头流稳定后再播放
        videoElement.play().then(() => {
          // 流稳定后显示视频
          videoElement.style.opacity = 1
        }).catch(err => {
          console.error('视频播放失败:', err)
          this.$toast('请点击屏幕激活摄像头')
        })
        } catch (error) {
          console.log('摄像头访问被拒绝')
          // this.$toast('摄像头访问被拒绝')
        }
      }
    },
    lastto() {
      this.$router.go(-1)
      this.stopStream()
    },
    // 修改部分
    async setdevicestwo() {
      try {
        // 改用 facingMode 模式切换
        const facingMode = this.currentText ? "user" : "environment"
        await this.switchCamera(facingMode)
        this.currentText = !this.currentText
      } catch (error) {
        console.error('切换摄像头失败:', error)
        this.$toast('摄像头切换失败,请确保已授权相机权限')
      }
    },
    gtouchstart() {
      // //真正长按后应该执行的内容
      this.isRecording = true
      this.startProgress()
      this.startRecording()
      return false
    },
    //手释放,如果在500毫秒内就释放,则取消长按事件,此时可以执行onclick应该执行的事件
    gtouchend() {
      this.isRecording = false
      this.progress = 0
      clearInterval(this.interval)
      this.stopRecording()
      return false
    },
    startProgress() {
      this.interval = setInterval(() => {
        if (this.progress >= 100) {
          clearInterval(this.interval)
          this.interval = null
          return
        }
        this.progress += 1
      }, 300)
    },
    // 开始录制或停止录制
    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 = blob
          // this.typefilevideo = URL.createObjectURL(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
    },
    async switchCamera(facingMode) {
      try {
        this.stopStream()

        // 暂时隐藏视频
        const videoElement = this.$refs.video
        videoElement.style.opacity = 0

        // 获取所有视频输入设备
        const devices = await navigator.mediaDevices.enumerateDevices()
        const videoDevices = devices.filter(device => device.kind === 'videoinput')

        let constraints = {
          video: {
            facingMode: { ideal: facingMode },
            width: { ideal: 1280 },
            height: { ideal: 720 },
            frameRate: { ideal: 30 }
          }
        }

        // 对于安卓设备,如果是后置摄像头且有多个摄像头,则使用第二个摄像头
        if (!this.$store.state.browser.ios && facingMode === 'environment' && videoDevices.length > 1) {
          constraints.video.deviceId = { exact: videoDevices[3].deviceId }
        } else if (this.$store.state.browser.ios && facingMode === 'environment' && videoDevices.length > 1) {
          constraints.video.deviceId = { exact: videoDevices[2].deviceId }
        } else {
          if (facingMode === 'user') {
            videoElement.style.transform = 'scaleX(-1)'  // 镜像效果
          } else {
            videoElement.style.transform = 'scaleX(1)'   // 去掉镜像效果
          }
          constraints.video.deviceId = { exact: videoDevices[0].deviceId }
        }

        // 如果是后置摄像头,确保不使用变焦设置
        constraints.video.zoom = 1
        constraints.video.focusMode = 'continuous'


        this.mediaStream = await navigator.mediaDevices.getUserMedia(constraints)
        videoElement.srcObject = this.mediaStream

        // 等待摄像头流稳定后再播放
        videoElement.play().then(() => {
          // 流稳定后显示视频
          videoElement.style.opacity = 1
        }).catch(err => {
          console.error('视频播放失败:', err)
          this.$toast('请点击屏幕激活摄像头')
        })

      } catch (error) {
        console.error('摄像头访问失败:', error)
        if (error.name === 'OverconstrainedError') {
          this.$toast('当前设备不支持该摄像头')
        } else {
          this.$toast('摄像头访问被拒绝')
        }
        throw error
      }
    },
    stopStream() {
      if (this.mediaStream) {
        this.mediaStream.getTracks().forEach(track => {
          track.stop()
          this.mediaStream.removeTrack(track)
        })
        this.mediaStream = null
      }
    },
    // 重新录制
    clearvideo() {
      this.isRecording = false
      this.progress = 0
      this.timernum = 20
      this.recordedVideoUrl = null
      clearInterval(this.interval)
    },
    // 发布
    getto() {
      let _this = this
      _this.$loadingService.show({}, true)
      _this.axios
        .post(_this.$store.state.api.qywx_admin_api + "/upload/video_blob", this.recordedVideoUrl, {
          headers: {
            token: _this.token,
          },
        })
        .then(function (res) {
          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
              }
            }
            _this.$loadingService.hide()
            if (res.data.status == 1) {
              _this.stopStream()
              if (_this.$route.query.path == 'addFriend') {
                let obj = {
                  fileUrl: res.data.data.path,
                  type: 'video'
                }
                sessionStorage.setItem('addFriendvideo', JSON.stringify(obj))
              }
              _this.$router.go(-1)
              _this.timernum = 20
            } else {
              _this.timernum = 20
              _this.$toast(res.data.msg || "出现了一些问题")
            }
          } else {
            _this.timernum = 20
            _this.$toast(res.data.msg ? res.data.msg : "请求失败")
          }
        })
        .catch(function (error) {
          _this.$loadingService.hide()
          _this.timernum = 20
          console.log(error)
          _this.$toast("接口异常,出现了一些问题")
        })
    }
  }
}
</script>

<style scoped lang='less'>
.top_fixed {
  position: fixed;
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: #fff;
  z-index: 100;
  width: 100%;
  max-width: 640px;
  margin: 0 auto;
  padding: 10px;
}

.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 {
    width: 50px;
    height: 50px;
    padding: 10px;
    text-align: center;
    background: #858585;
    border-radius: 50px;
  }
}

.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;
  transition: opacity 0.5s ease-in-out;
}

.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>


请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

黑白两客

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值