<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>
自定义vue摄像头 自定义时长,自定义大小
于 2025-02-07 18:41:04 首次发布