VUE2实现录像(PC端)可拍照 下载 预览 选取指定摄像头和麦克风设备 源代码可直接使用 原创。
2022-10-16 17:57:44。

嗯呐FT 
码龄1年
关注
2022.10月份 谷歌是最新版本 使用vue2写 。

下面的这些代码可以在谷歌浏览器的本地(localhost)和https环境下执行。中间有大量的注释可查看,基本上看一遍就懂,前面的坑我基本都踩过了。
如果是http环境的话,我下面这些代码是执行不了的,因为我做了获取指定设备的功能,如果要http环境中使用,需要把获取指定设备的相关代码删掉才能用,而且需要去谷歌的这个地址配置一下才能用,最下面我会介绍怎么在http环境中用。
功能如下:
1.可获取多个摄像头设备并选择其一进行录制。
2.录制的过程中拔掉摄像头监听结束录制。
3.可在录制中进行拍照生成png图片 可预览 可下载。
4.录制完成会生成一个有声音和画面的webm格式视频 可在线预览下载。
5.录制的过程中计时(最小误差)
界面展示(界面我就做个小Demo 没有过多去弄好看点 重要是script部分)

先上代码 -- html部分
<template>。
<div class="publish">。
<div class="box">。
<div class="videoPart">。
<div class="videoRecord" @mouseenter="hoverVideo(0)" @mouseleave="hoverVideo(1)">。
<video id="videoCamera" :width="videoWidth" :height="videoHeight" autoPlay></video>。
<div class="hoverVideoOutside" v-if="ifHoverVideo && ifStartRecord" >。
<div class="hoverVideoInside">。
<div class="hoverVideoInsideInside">。
<div class="hoverVideoBtn"><div class="hoverVideoBtnInside"></div></div>。
<span>{{recordHMSTime}}</span>。
</div>。
</div>。
</div>。
</div>。
<canvas id="canvasCamera" class="canvas" :width="videoWidth" :height="videoHeight"></canvas> 。
<video v-if="showVideo" :src="videoSrc" autoplay controls></video>。
<img v-if="showImg" class="imgClass" :src="imgSrc" alt="">。
</div>。
<div>。
<el-select v-model="deviceId" placeholder="请选择摄像头" @change="selectVideoChange" @focus="findVideoDevice" :disabled="ifStartRecord">。
<el-option。
v-for="item in deviceArr"。
:key="item.deviceId"。
:label="item.label"。
:value="item.deviceId">。
</el-option>。
</el-select>。
<el-button v-if="!ifStartRecord" @click="startRecord" icon="el-icon-video-camera" size="small">开始录制</el-button>。
<el-button v-else @click="stopRecord" icon="el-icon-switch-button" size="small">结束录制</el-button>。
<el-button @click="photographBtn" icon="el-icon-camera" size="small">拍照</el-button>。
</div>。
<vxe-table。
border
resizable。
show-overflow。
ref="xTable"。
height="500"。
:row-config="{isHover: true}"。
:data="tableData">。
<vxe-column type="seq" width="60"></vxe-column>。
<vxe-column field="name" title="Name"></vxe-column>。
<vxe-column title="操作" width="200" show-overflow>。
<template #default="{ row }">。
<vxe-button type="text" icon="vxe-icon-edit" @click="preview(row)">预览</vxe-button>。
<vxe-button type="text" icon="vxe-icon-edit" @click="download(row)">下载</vxe-button>。
</template>。
</vxe-column>。
</vxe-table>。
</div> 。
</div>
</template>。
CSS代码
<style scoped>。
.canvas{
opacity: 0!important;。
}
.videoPart{
position: relative;。
display: flex;。
align-items: center;。
}
.hoverVideoOutside{。
position: absolute;。
top: 0px;
left: 0px;
width: 500px;。
height: 300px;。
background:linear-gradient(#000,transparent 20%);。
z-index: 999;。
}
.hoverVideoInside{。
position: relative;。
width: 100%;。
height: 100%;。
}
.hoverVideoInsideInside{。
position: absolute;。
top: 10px;
left: 380px;。
padding: 2px;。
display: flex;。
justify-content: center;。
align-items: center;。
}
.hoverVideoBtn{。
width: 30px;。
height: 30px;。
border-radius: 50%;。
border: 2px solid red;。
padding: 3px;。
box-sizing: border-box;。
margin-right: 4px;。
}
.hoverVideoBtnInside{。
background: red;。
width: 20px;。
height: 20px;。
border-radius: 50%;。
box-sizing: 50%;。
}
span{
color: #fff;。
}
.videoRecord{。
position: relative;。
}
.imgClass{
width: 500px;。
height: 300px;。
}
</style>
逻辑部分
<script>
export default {。
name: 'HelloWorld',。
data() {
return {
ifOpenCamera: false,//控制摄像头开关。
ifStartRecord:false, //是否开始录制。
thisVideo: null,。
thisContext: null, //canvas。
thisCanvas: null,。
videoWidth: 500,。
videoHeight: 300,。
videoCecorded: [], //接受的数据流。
mediaRecorderData: {},。
videoSrc:'', //录制完的视频预览。
imgSrc:'',//录制完预览的图片地址。
deviceArr:[],//获取该电脑的摄像头。
deviceId:'', //选择哪个摄像头。
tableData:[], //录制完 制作表格数据。
showVideo:false, //录制完预览视频。
showImg:false,//录制完预览图片。
recordTime:0, //监听录像的时间。
recordHMSTime:'00:00:00', //监听录像的时间。
ifHoverVideo:false, //监听是否鼠标在视频的上面 显示录制时间。
timer:null, //每一秒执行一次计算录像的时间 这个是有误差的。
startTime:'', //记录开始录制的时间戳。
stream:null,。
}
},
created() {
this.initDevice();。
},
methods: {
initDevice(){。
this.$nextTick(() => {。
this.videoCecorded = []。
this.mediaRecorderData = null。
this.thisCanvas = document.getElementById('canvasCamera');。
this.thisContext = this.thisCanvas.getContext('2d');。
this.thisVideo = document.getElementById('videoCamera');。
// 旧版本浏览器可能根本不支持mediaDevices,我们首先设置一个空对象。
console.log('navigator.mediaDevices', navigator.mediaDevices)。
if (navigator.mediaDevices === undefined) {。
navigator.mediaDevices = {}。
this.dialogVisible = !this.dialogVisible;。
console.log('http环境下没开那个谷歌权限 所以有点问题 得弹出提示弹窗 如果是本地或者https环境就基本不会走到这里')。
this.$message({。
message: '去谷歌浏览器配置网址',。
type: 'error'。
})
}
// 一些浏览器实现了部分mediaDevices,我们不能只分配一个对象。
// 使用getUserMedia,因为它会覆盖现有的属性。
// 这里,如果缺少getUserMedia属性,就添加它。
if (navigator.mediaDevices.getUserMedia === undefined) {。
navigator.mediaDevices.getUserMedia = function (constraints) {。
// 首先获取现存的getUserMedia(如果存在)。
let getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.getUserMedia;。
// 有些浏览器不支持,会返回错误信息。
// 保持接口一致。
if (!getUserMedia) {。
return Promise.reject(new Error('getUserMedia is not implemented in this browser'))。
}
// 否则,使用Promise将调用包装到旧的navigator.getUserMedia。
return new Promise(function (resolve, reject) {。
getUserMedia.call(navigator, constraints, resolve, reject)。
})
}
}
this.findVideoDevice()。
})
},
// 每次点开下拉框的时候都要去获取现在有多少个设备 以防在打开这个网站之后把摄像头拔了等情况 。
async findVideoDevice(){。
this.deviceId = ''。
this.deviceArr = []。
const _this = this。
try{
// 获取你电脑中有什么设备。
let deviceArr = await navigator.mediaDevices.enumerateDevices();。
if(this.deviceArr.length == 0){。
this.$message({。
message: '没有摄像头设备',。
type: 'error'。
})
this.deviceArr = []。
return 。
}
console.log('设备',deviceArr)。
deviceArr.forEach(item=>{。
if(item.kind == 'videoinput'){。
this.deviceArr.push(item) //获取你电脑中有多少个摄像头设备。
}
})
}catch(error){。
console.log(error)。
this.deviceArr = []。
}
},
// 选择完是哪个摄像头以后执行的方法。
selectVideoChange(){。
this.$nextTick(() => {。
const _this = this;。
const constraints = {。
audio: true,。
video: { 。
width: _this.videoWidth, 。
height: _this.videoHeight, 。
transform: 'scaleX(-1)',。
deviceId:{exact:this.deviceId} 。
},
};
navigator.mediaDevices.getUserMedia(constraints).then(async(stream)=>{。
// 旧的浏览器可能没有srcObject。
if ('srcObject' in _this.thisVideo) {。
_this.thisVideo.srcObject = stream。
} else {。
// 避免在新的浏览器中使用它,因为它正在被弃用。
_this.thisVideo.src = window.URL && window.URL.createObjectURL(stream)。
} 。
_this.thisVideo.onloadedmetadata = function (e) {。
_this.thisVideo.play()。
} 。
_this.ifOpenCamera = true 。
}).catch(err => {。
this.errReponse(err)。
});
});
},
// 开始录制
startRecord() {。
if(this.deviceId == ''){。
this.$message({。
message: '请选择摄像头',。
type: 'error'。
})
return
}
const _this = this。
this.mediaRecorderData = null。
this.videoCecorded = []。
this.$nextTick(()=>{。
const constraints = {。
audio: true,。
video: { 。
width: this.videoWidth, 。
height: this.videoHeight, 。
transform: 'scaleX(-1)',。
deviceId:{exact:this.deviceId} //选取指定的设备来录制。
},
};
//必须在model中render后才可获取到dom节点,直接获取无法获取到model中的dom节点。
navigator.mediaDevices.getUserMedia(constraints).then(function (stream) {。
_this.stream = stream。
// 这个要重写一次 。
// 我插第三方的视频录像设备的情况下 点击录像 然后把设备拔出来 这个时候画面是黑色的 。
// 如果没有重写下面的方法 插进设备后 点击录像 画面依然是黑色的 但是能录像 只是界面上不显示 。
if ('srcObject' in _this.thisVideo) {。
_this.thisVideo.srcObject = stream。
} else {。
// 避免在新的浏览器中使用它,因为它正在被弃用。
_this.thisVideo.src = window.URL && window.URL.createObjectURL(stream)。
}
_this.thisVideo.onloadedmetadata = function (e) {。
_this.ifOpenCamera = true 。
_this.thisVideo.play()。
}
_this.startRecording(stream);//调用录制控件方法,触发开始录制。
}).catch(err => {。
// 点录制之前断开设备连接 但选择框已经选了设备 就会触发这个err。
this.errReponse(err)。
});
})
},
//拍照按钮
photographBtn() {。
//先判断是否开启了摄像头。
if(!this.ifOpenCamera){。
this.$message({。
message: '摄像头都还没开呢 傻猪猪',。
type: 'error'。
})
return
}
// 点击,canvas画图。
this.thisContext.drawImage(this.thisVideo, 0, 0, this.videoWidth, this.videoHeight);。
// 如果图片尺寸不想 500 300 那就写下面这个。
// this.thisContext.drawImage(this.thisVideo, 0, 0, this.videoWidth, this.videoHeight,0,0,1000, 600); 。
const fileName = (new Date).toISOString().replace(/:|\./g,'-')。
const a = this.thisCanvas.toDataURL('image/png')。
const file = this.dataURLtoFile(a,fileName + 'png')。
this.tableData.push({。
id:2,
name:fileName + 'png',。
url:this.thisCanvas.toDataURL('image/png'),。
type:'png'。
})
},
//停止录制
stopRecord() {。
if (this.thisVideo && this.thisVideo !== null) {。
this.mediaRecorderData.stop(); //结束录制。
}
},
// 开始录制中
startRecording(stream) {。
console.log('stream 在开始的时候的',stream)。
let _this = this。
this.mediaRecorderData = new MediaRecorder(stream, {。
mimeType: 'video/webm;codecs=vp8,opus' //不加这个codecs=vp8,opus有时候下载下来之后看几秒就没了 残缺的视频。
});
this.mediaRecorderData.addEventListener("dataavailable", (e) => {。
if (e.data.size > 0) {。
_this.videoCecorded.push(e.data);//视频录制视频流数据 。
};
});
this.mediaRecorderData.addEventListener("stop", () => {。
console.log("结束录制");。
_this.updataVideo();//上传实时录制的视频。
_this.ifStartRecord = false。
_this.recordTime = 0。
_this.recordHMSTime = '00:00:00'。
clearTimeout(_this.timer)。
_this.timer = null。
});
this.mediaRecorderData.addEventListener("start", (e) => {。
console.log("开始 录制");。
_this.ifStartRecord = true。
_this.recordTime = 0。
_this.startTime = new Date().getTime();。
_this.timer = setTimeout(_this.fixed,1000)。
});
this.mediaRecorderData.start()。
},
// 上传录制视频方法,获取视频地址。
updataVideo() {。
const blob = new Blob(this.videoCecorded, {。
type: 'video/webm'。
});
const fileName = (new Date).toISOString().replace(/:|\./g,'-')。
this.tableData.push({。
id:2,
type:'webm',。
name:fileName + '.webm',。
url:URL.createObjectURL(blob)。
})
},
// 预览视频 图片
preview(row){。
if(row.type == 'png'){。
this.showImg = true。
this.showVideo = false。
this.imgSrc = row.url。
}else if(row.type == 'webm'){ 。
this.showVideo = true。
this.showImg = false。
this.videoSrc = row.url。
}else{
this.$message({。
message: '无法预览',。
type: 'error'。
})
}
},
download(row){。
let aTag = document.createElement('a');//创建一个a标签。
aTag.download = row.name;。
aTag.href = row.url;。
aTag.click();。
},
hoverVideo(num){。
if(num){
// 移出
this.ifHoverVideo = false。
}else{
// 移入
this.ifHoverVideo = true。
}
},
secondChangeMinute(second){。
const hour = parseInt(second / 3600)。
const min = parseInt(second / 60)。
const se = parseInt(second % 60)。
const cHour = hour<10?'0'+hour:hour。
const cMin = min<10?'0'+min:min。
const cSe = se<10?'0'+se:se。
return cHour + ':' + cMin + ':' + cSe。
},
fixed(){
this.recordTime += 1。
this.recordHMSTime = this.secondChangeMinute(this.recordTime)。
if(!this.stream.active){。
this.stopRecord()。
}
var offset = new Date().getTime() - (this.startTime + this.recordTime * 1000);。
var nextTime = 1000 - offset;。
if (nextTime < 0) nextTime = 0;。
this.timer = setTimeout(this.fixed, nextTime);。
},
errReponse(err){。
const message = err.message || err。
const response = {。
'permission denied': '浏览器禁止本页面使用摄像头或麦克风,请开启相关的权限',。
'requested device not found': '未检测到摄像头'。
}
console.log(response[ message.toLowerCase() ] || '未知错误');。
this.$message({。
message: response[ message.toLowerCase() ] || '未知错误',。
type: 'warning'。
})
},
dataURLtoFile: function(dataurl, filename) { 。
let 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 File([u8arr], filename, { type: mime });。
},
// 这个方法是给需要的人看的 演示打开摄像头的方法。
async openCamera(){。
await navigator.mediaDevices.getUserMedia({audio:true,video:true}).then(()=>{。
//我这个时候已经打开摄像头了。
}).catch(err=>{。
this.errReponse(err)。
})
},
// 这个方法是给需要的人看的 演示关闭摄像头的方法。
closeCamera(){。
this.thisVideo.srcObject.getTracks().forEach(item => {。
// 关闭当前所有已打开的设备(你刚刚选择的摄像头设备和你默认的麦克风)
item.stop()。
})
},
}
</script>。
问题解答区域:
1.录制的视频只能是webm格式吗?进度条呢?
答:我在写程序的时候发现只能写webm格式,如果是写MP4或者其他类型,视频下载下来是没有办法播放的,可能在移动端就没有问题,我还没有去尝试在移动端做这个功能。webm格式我所知道的有两个弊端,下载下来的时候无法拖动进度条,而且要在指定播放器中才能播放。如果想要解决这个进度条的问题,可以去看这个地址的解决方法。前端 mediaRecorder 录制视频源代码实例,和本地播放器无法定位进度条问题分析和解决_anne都的博客-CSDN博客_mediarecorder 进度条。
2.http问题
答:在http环境下是可以录制的,但是我上面的代码运行不了就是了。因为我添加了一个获取用户电脑中设备的功能,该功能在http环境下没有办法获取,可能是浏览器出于安全的问题。如果想要在http环境下录制,可以参考下面的这个地址,我也是百度了很久,看了很多别人的博客才得出的最终代码结果。地址如下:
=>1.JS调用媒体设备失败 --- getUserMedia undefine 问题(各浏览器配置方法)_<!--玄德-->的博客-CSDN博客_浏览器不支持getusermedia。
=>2.
PC端调用摄像头录制视频——vue标准写法_前端_森森的博客-CSDN博客_vue调用摄像头录像。
=>3.
JS基于页面实现音视频的录制(一)_画虎成鳖的博客-CSDN博客_js录制视频。
3.表格不一定要用vxetable,随便用 。
4.如果还有什么问题再在评论区问吧。。
盒子模型概述:
所有HTML元素都可以看作盒子,在CSS中,"box model"这一术语是用来设计和布局时使用。CSS盒模型本质上是一个盒子,封装周围的HTML元素,它包括:边距,边框,填充,和实际内容。盒模型允许我们在其它元素和周围元素边框之间的空间放置元素。下面的图片说明了盒子模型(Box Model):
盒子模型组成以及对应区域
盒子模型的组成:
Margin(外边距) - 清除边框外的区域,外边距是透明的。
Border(边框) - 围绕在内边距和内容外的边框。
Padding(内边距) - 清除内容周围的区域,内边距是透明的。
Content(内容) - 盒子的内容,显示文本和图像。
以上是html中盒子模型的结构,每个元素都可以用这个盒子模型来解析。在开发中,一个元素的样式表现形式,也是由这个盒子模型的每个部分来表现的。对应到css中样式的属性有一下几个纬度——
width, height, padding, border, margin。由于在不同的浏览器中,这几个属性所表示的盒子模型中的部分有所差异,所以又分为了标准盒子模型和怪异盒子模型。
一,标准盒子模型
解释:在标准盒子模型下
(1)css中的width, height属性,分别表示的是盒子模型中content的宽度和高度。
(2)css中的padding,表示的是盒子模型的的padding部分。
(3)css中的border,表示的是盒子模型中的border部分。
(4)css中的margin,表示的是盒子模型中的margin。
二,怪异盒子模型(ie盒子模型)
解释:在怪异盒子模型下
(1)css中的width, height属性,分别表示的是盒子模型中content的宽度和高度加上盒子模型中padding和border的宽度。
(2)css中的padding,表示的是盒子模型的的padding部分。
(3)css中的border,表示的是盒子模型中的border部分。
(4)css中的margin,表示的是盒子模型中的margin。
**段落性总结 —— 标准盒子模型和怪异盒子模型的区别**。
从上述的内容可以看出,标准盒子模型和怪异盒子模型的区别,完全体现在css中width和height这两个属性对盒子模型的表现上。
标准盒子模型:css中width/height=content的width/height。
怪异盒子模型:css中width/height=content的width/height+padding+border。
导致的结果:
由于标准盒子模型和怪异盒子模型的存在,这就导致当同一段css代码作用在同一个元素上时,在不同盒子模型下的浏览器中,元素所占的宽度和高度却不同。
三,css3中box-sizing下的盒子模型。
语法:(属性)box-sizing: (属性值)content-box/border-box/inherit;
box-sizing对盒子模型的影响:
(1)当“box-sizing“的值为”content-box“时,css中的width所包含的部分是盒子模型中content的宽度。此时和标准盒子模型表现一致。
(2)当“box-sizing“的值为”border-box“时,css中的width所包含的是盒子模型中的content的宽度+padding+border的宽度。此时和怪异盒子模型的表现一致。
总结:box-sizing属性,让开发人员可以控制浏览器的是以标准盒子模型表现,还是以怪异盒子模型表现。
box-sizing的兼容性:
到此盒子模型的所有内容全部阐述完毕。
最佳方案:由于box-sizing的兼容性在ie8及以上,那在不要求兼容ie8一下的项目中,可以使用box-sizing给所有元素统一设置盒子模型的表现形式。个人推荐:。
*{box-sizing:'border-box'}。
这样只要设置的width的宽度,就是这个元素在页面中真实的宽度了。不用再去计算padding和border对width的影响。
对于需要兼容ie8的项目,就需要针对不同的浏览器,添加特定的代码。或者避免指定宽度的元素border和padding的设置。
相关内容:
1.行内元素之间的水平margin,即两个元素margin之和。
2.块级元素之间的竖直margin, 即取最大的。
3.嵌套盒子之间的margin,即父元素的padding+子元素的margin。
4.margin可以设置为负值。
不同浏览器的css前缀:Element {。
-moz-box-sizing: content-box;。
-webkit-box-sizing: content-box;。
-o-box-sizing: content-box;。
-ms-box-sizing: content-box;。
box-sizing: content-box;。
参考:
w3c标准:https://www.runoob.com/cssref/css3-pr-box-sizing.html。