目录
最近项目开发中,需要实现在video 视频上截图,录影后将文件上传到阿里云服务器上。截图功能相对来说比较容易实现,使用canvas 的 drawImage 方法将video 控件的区域绘制下来即可。录影相对来说比较麻烦,目前用webRTC 简单实现。
功能简单介绍:使用videojs来播放海康NVR 的Dash视频流,需要针对实时画面进行截取,或者录制10s的视频片段然后上传至阿里云上。
截图:截取图片后可以进行涂鸦编辑,并能清除,撤销。
//截取当前帧的图片
cutPicture(){
let self=this;
self.showCancelContent=false; // 在点击video 上的截图按钮隐藏取消编辑区域。
if(self.sourceList.length>=5){
self.notify('最多上传5个资源!','warning',3000);
return false;
}
if(self.fullScreen){ //视频如果处于全屏状态,退出全屏
self.exitFullscreen();
self.fullScreen=false;
}
self.showCutDialog=true;
self.$nextTick(()=>{
self.canvasEl=document.getElementById('icanvas');
var ctx = self.canvasEl.getContext('2d');
ctx.drawImage(self.videoEl,0,0,767*self.percentHeight,431*self.percentHeight);
var oGrayImg=self.canvasEl.toDataURL('image/jpeg'); // 这里通过toDataURL 获取图片的URL连接链接
self.imageCanvas.src=oGrayImg;
let imgObj=new Image();
imgObj.src=oGrayImg; //新建一个image 对象把初始的canvas.src 添加到数组中。
self.imageCanvasList.push(imgObj);
})
},
涂鸦编辑:通过监听canvas上的鼠标按下,移动,抬起事件来进行绘制。
//给canvas注册mousedown mousemove 事件
mouseDownAction(e){
let self=this;
self.isMouseDown=true;
self.X=e.offsetX;
self.Y=e.offsetY;
self.showPenBtn=false;
},
mouseMoveAction(e){
let self=this;
if(self.isMouseDown){
self.X1=e.offsetX;
self.Y1=e.offsetY;
self.showPenBtn=false; //鼠标移动的时候隐藏画笔区域
self.drawLine(self.X,self.Y,self.X1,self.Y1);
self.flag++;
}
},
mouseUpAction(e){
let self=this;
self.isMouseDown=false;
self.showPenBtn=true;
self.showCancelContent=true; //每次鼠标弹起后显示撤销区域,画笔区域
if(self.flag!=0&&self.canvasEl!=''){
let imgObj=new Image();
imgObj.src=self.canvasEl.toDataURL("image/jpeg");
self.imageCanvasList.push(imgObj); //每编辑一次就存入数组中
}
self.flag=0;
},
drawLine(x,y,x1,y1){
let self=this;
var ctx=self.canvasEl.getContext('2d');
if(self.flag){
ctx.beginPath();
}
ctx.moveTo(x,y);
ctx.lineWidth=4;
ctx.strokeStyle=self.penChecked;
ctx.lineTo(x1,y1);
ctx.stroke();
if(self.flag!=0){
self.X=self.X1;
self.Y=self.Y1;
}
},
涂鸦撤销清除功能:在清空canvas的同时,把数组中存放的image 再绘制到canvas上。
// 清空canvas 上所有的涂鸦信息
removeEditCanvas(){
let self=this;
self.showCancelContent=false;
self.canvasEl=document.getElementById('icanvas');
var ctx = self.canvasEl.getContext('2d');
ctx.clearRect(0,0,767*self.percentHeight,431*self.percentHeight);
ctx.drawImage(self.imageCanvasList[0],0,0,767*self.percentHeight,431*self.percentHeight);
self.imageCanvasList=[];
},
// 撤销一次涂鸦
cancleEditCanvas(){
let self=this;
self.showCancelContent=false;
self.imageCanvasList.pop();
self.canvasEl=document.getElementById('icanvas');
var ctx = self.canvasEl.getContext('2d');
ctx.clearRect(0,0,767*self.percentHeight,431*self.percentHeight);
if(self.imageCanvasList.length==0){
ctx.drawImage(self.imageCanvas,0,0,767*self.percentHeight,431*self.percentHeight);
}
else{
ctx.drawImage(self.imageCanvasList[self.imageCanvasList.length-1],0,0,767*self.percentHeight,431*self.percentHeight);
}
},
最终点击确认添加到下方的sourceList 中。
录影:点击按钮后录制10s左右的视频文件。
import RecordRTC from '../../../static/RecordRTC.js'
getVideo(){
let self=this;
self.showGetVideo=true; //显示录制按钮
self.videoSpeed=0;
self.$nextTick(()=>{
self.startTimeCutVideo=new Date().getTime();
self.computeFrame();
self.looper();
setTimeout(()=>{
var btn_canvas = document.getElementById("btn-graph-canvas");
self.drawBtn(btn_canvas, 100, "#f31d65", "#f31d65"); //在开始录制的同时显示录制进度的按钮。
},1000)
})
},
looper(){
let self=this;
if(!self.isRecordingStarted){
self.timeVideo=setTimeout(self.looper, 0);
}
else{
self.endTImeCutVideo=new Date().getTime();
if((self.endTImeCutVideo-self.startTimeCutVideo)/1000>11){
clearTimeout(self.timeVideo);
self.showGetVideo=false;
self.isRecordingStarted=false;
self.isREC=false;
setTimeout(()=>{
self.addVideoToList();
},100)
}
else{
self.isREC=true;
html2canvas(self.videoEl).then(function(canvas){
var ctx = self.canvasEl.getContext('2d');
let width=self.varyWindowWidth*0.418;
let height=self.varyWindowWidth*0.288;
ctx.clearRect(0, 0, width, height);
ctx.drawImage(self.videoEl,0,0,width,height);
if(self.isStoppedRecording) {
return;
}
requestAnimationFrame(self.looper);
})
}
}
},
computeFrame(){
let self=this;
self.canvasEl=document.getElementById('vcanvas');
var ctx = self.canvasEl.getContext('2d');
self.recorder = RecordRTC(self.canvasEl, {
type: 'canvas'
});
self.isStoppedRecording =false;
self.isRecordingStarted = true;
self.recorder.startRecording(); //开始录制
},
绘制录制按钮:参考网上大神绘制环形进度条的代码做的。使用计时器每秒绘制10%的长度。
drawMain(drawing_elem, percent, forecolor, bgcolor) {
/*
@drawing_elem: 绘制对象
@percent:绘制圆环百分比, 范围[0, 100]
@forecolor: 绘制圆环的前景色,颜色代码
@bgcolor: 绘制圆环的背景色,颜色代码
*/
let self=this;
var context = drawing_elem.getContext("2d");
var center_x = drawing_elem.width / 2;
var center_y = drawing_elem.height / 2;
var rad = Math.PI*2/100;
// 绘制背景圆圈
function backgroundCircle(){
context.beginPath();
context.lineWidth = 14; //设置线宽
var radius = center_x - context.lineWidth;
context.arc(center_x, center_y, radius, 0, Math.PI*2, false);
context.fillStyle=bgcolor;
context.globalAlpha = 0.5;
context.fill();
}
//绘制运动圆环
function foregroundCircle(n){
context.save();
context.strokeStyle = forecolor;
context.globalAlpha = 1;
context.lineWidth = 6;
context.lineCap = "round";
var radius = center_x - context.lineWidth;
context.beginPath();
context.arc(center_x, center_y, radius , -Math.PI/2, -Math.PI/2 +n*rad, false); //用于绘制圆弧context.arc(x坐标,y坐标,半径,起始角度,终止角度,顺时针/逆时针)
context.stroke();
context.closePath();
context.restore();
}
//绘制文字
function text(n){
context.save();
context.fillStyle='white';
context.globalAlpha = 1;
var font_size=self.btnFontSize;
context.font='bold '+font_size+'px Helvetica';
var textStr='';
if(n==100){
textStr='录制成功';
}
else{
textStr='正在录制';
}
var text_width = context.measureText(textStr).width;
context.fillText(textStr,center_x-text_width/2,center_y+font_size/2);
context.restore();
}
//执行动画
function drawFrame(speed){
context.clearRect(0, 0, drawing_elem.width, drawing_elem.height);
backgroundCircle();
text(speed);
foregroundCircle(speed);
if(speed>=percent){
clearInterval(self.videoSpeedId);
}
}
self.videoSpeedId=setInterval(() => {
if(self.videoSpeed >= percent){
return;
}
else{
self.videoSpeed += 2;
drawFrame(self.videoSpeed);
}
}, 100);
},
在录影过程中,可以设置面板pointer-events 样式来防止用户在页面上进行操作,从而确保10s的视频录制完成。
pointer-events: none;
功能最终实现结果如下所示。
视频截图涂鸦的demo 如下地址
视频录影,如果视频质量较高录影卡顿就会非常明显,这个暂时没有办法解决。
文件上传服务器示例
这里首先申请阿里云OSS存储服务,具体参考网上教程。
使用canvas 的 toDataURL 方法可以将canvas对象转换为一个 data-URL地址。示例如下
1,首先画一个示例的canvas画板,然后获取对应的data-URL ,并给到一个image对象。
let iCanvas = document.getElementById('canvas');
var getCanvas = function() {
if (!iCanvas) {
iCanvas = document.createElement('canvas');
iCanvas.width = 500;
iCanvas.height = 300;
iCanvas.style.position = 'absolute';
iCanvas.style.left = 0;
iCanvas.style.top = 0;
var ctx = iCanvas.getContext('2d');
ctx.fillStyle = 'blue';
ctx.fillRect(10, 10, 300, 300);
document.body.append(iCanvas);
}
};
getCanvas();
var getImageSrc = function() {
var src = iCanvas.toDataURL(); // toDataURL 默认参数是image/jpeg, base64位的编码
var imgObj = new Image();
imgObj.src = src; // 可以得到一个image对象
iCanvas.toBlob(blob => {
imgObj.file = blob; // blob 文件可以通过formData格式传参直接上传服务器
var url = URL.createObjectURL(blob); // 用于URL的File对象,Blob对象。
});
}
通过canvas的toBlob 方法创造Blob对象,type指定图片格式,默认格式为image/png
canvas.toBlob(callback, type, encoderOptions);
2,上传到oss存储
上传文件前需要把我们上传的文件转换成 Blob,File 格式。
其中File 继承自Blob类,是特殊的Blob,可以用在任意的Blob类型Context 中,即Blob适用的场景File也可以。
// 阿里云OSS存储
var uploadImg = (fileObj) => {
const OSS = require('ali-oss'); // 引入阿里云的包
const client = new OSS({
region,
accessKeyId,
accessKeySecret,
bucket,
});
client.multipartUpload(fileObj.fileName, fileObj.file).then((result) => {
const fileUrl = result.name; // 最终可得到一个上传成功后的文件云端路径,我们可以把这个地址存储在数据库中,当需要查看上传的图片时再进行解析即可。
});
};
3,使用formData通过请求后端接口上传
var uploadItem = (fileObj) => {
var formData = new FormData(); // 获取一个formData 对象
formData.append('file', fileObj.file);
axios.post('http://....', formData).then(res => { console.log(res);}); //请求接口进行上传
}
可以看到formData 格式中的file有了时间戳等信息。
4,File 格式上传服务器
通常我们可以使用input type="file" 进行上传,我们可以通过inputfile action 直接上传,也可以通过FormData 格式调用接口上传。
<input type="file"
multiple="multiple"
id="avatar" name="avatar"
accept="image/png, image/jpeg">
<script>
var inputDom = document.getElementId('avatar');
var fileList = Array.from(inputDom.files);
// 由于fileList 是一个FileList类型 因此我们可以将其转为Array类型进行遍历。
fileList.forEach(file => {
uploadItem(file); // 这里即可以把这个file转为formData 格式然后手动请求后端接口
});
</script>