前端监测flvjs视频流并实现截屏涂鸦功能

1,806 阅读3分钟

前言

又入手新的项目啦,需求是将视频监控在前端页面上展示,并对可对监控进行截屏和涂鸦操作。一听就是我没有实现过的功能啊,瞬间激起了我的兴趣。开心~~


1、视频流的引入

了解flv.js

Flv.js是HTML Flash视频(FLV)播放器,纯原生JavaScript开发,没有用到Flash。由bilibili网站开源。

它的工作原理是将FLV文件流转码复用成ISO BMFF(MP4碎片)片段,然后通过Media Source Extensions将MPA喂进浏览器。

安装flv.js

npm install --save flv.js

使用

<template>
	<div ref="video">
        <video id="video"/>
    </div>
    <div @click="tackPic">
        快拍
    </div>
    
</template>
//js部分
import flvjs from 'flv.js'  //单页面引用
export default{
    data(){
        return {
            player:null
        }
    },
    mounted(){
        this.initPlayer();
    },
    methods:{
        initPlayer(){
            const video = document.getElementById("video");
            this.player = flvjs.createPlayer(
                {
                    type: "flv",
                    isLive: true,
                    hasAudio: false,
                    url: 'http://*******.flv'//视频流文件
                },
                {
                    enableWorker: true,
                    enableStashBuffer: false,
                    stashInitialSize: 120,
                    enableDurationMonitor: true
                }
            );
            player.attachMediaElement(video);
            player.load();
            player.play();
        }
    },
    beforeDestroy(){
        this.player.pause();
        this.player.unload();
        this.player.destroy();
        this.player.detachMediaElement();
        this.player = null
    }
    
}

2、保证视频流的稳定持续

在视频流的播放过程中,经常会出现视频流中断的问题,直观表现就是卡顿。仔细回忆下,是不是在观看直播过程中也会出现卡顿的情况,其实原理很简单,因为一些原因视频流播放发生中断的话,可进行重连。(后续更新具体操作。。。)

3、截屏保存

利用canvas进行截图

原理:其实就是在video地方新建相同位置大小的canvas标签,然后用canvas的“drawImage”方法,将视频部分保存下来。

click(){
    let canvas = document.createElement("canvas");//创建canvas标签
    let canvasCtx = canvas.getContext("2d");
    let video = document.getElementById('video');
    //设置canvas画布的宽和高,这一步很重要,决定截图是否完整
    canvas.width = video.offsetWidth;
    canvas.height = video.offsetHeight;
    //用drawImage方法将图片保存下来
    canvasCtx.drawImage(video, 0, 0, canvas.width, canvas.height);
    let img = canvas.toDataURL("image/jpeg");//将图片保存为base64格式
}

4、实现涂鸦功能

通过以上的努力,我们已经将图片拿到了。到这里,其实就相当于在一张图片上进行涂鸦并保存。

<template>
	<div>
        <canvas 
                :id="random" 
                width="400" 
                height="300" //这里的宽高比例根据截屏的图片进行设置上面video的宽高可按比例进行缩放
                @mousedown="canvasDown($event)"
                @mouseup="canvasUp($event)"
                @mousemove="canvasMove($event)"
                @touchstart="canvasDown($event)"
                @touchend="canvasUp($event)"
                @touchmove="canvasMove($event)"
                >
        </canvas>
        <div>
            <img :src="drawImg"/>
        </div>
    </div>
</template>

 const uuid = require('node-uuid')
	export defaut{
        data(){
            return{
                // canvas对象
                context: {},
                // 是否处于绘制状态
                canvasMoveUse: false,
                // 绘制矩形和椭圆时用来保存起始点信息
                beginRec: {
                    x: "",
                    y: "",
                    imageData: ""
                },
                // 储存坐标信息
      			drawInfo: [],
                img:new Image(),
                random:uuid(), //避免canvas缓存,动态设置id值
                url:'',//上面获取的图片的base64值,假装已经props过来
                drawImg:''//绘制的图片
            }
        },
        mounted(){
            this.initDraw();
        },
        methods:{
			initDraw(){
                const canvas = document.getElementById(this.random);
                this.context = canvas.getContext("2d");
                //初始化图片
                this.img.setAttribute("crossOrigin", "Anonymous");
      			this.img.src = this.url;
                this.img.onerror = () => {
                    var timeStamp = +new Date();
                    this.img.src = this.url + "?" + timeStamp;
                };
                this.img.onload = () => {
                	this.clean();
                };
                // 初始化画笔
               this.context.lineWidth = 4;
               this.context.strokeStyle = 'red';
           },
            // 鼠标按下
            canvasDown(e) {
              if (this.canDraw) {
                this.canvasMoveUse = true;
                // client是基于整个页面的坐标,offset是cavas距离pictureDetail顶部以及左边的距离
                let canvasX = e.offsetX;
                let canvasY = e.offsetY;
                // 记录起始点和起始状态
                this.beginRec.x = canvasX;
                this.beginRec.y = canvasY;
                this.beginRec.imageData = this.context.getImageData(
                  0,
                  0,
                  this.width,
                  this.height
                );
                // 存储本次绘制坐标信息
                this.drawInfo.push({
                  x: canvasX / this.width,
                  y: canvasY / this.height,
                  type: this.lineType
                });
              }
            },
            Area(p0, p1, p2) {
              let area = 0.0;
              area =
                p0.x * p1.y +
                p1.x * p2.y +
                p2.x * p0.y -
                p1.x * p0.y -
                p2.x * p1.y -
                p0.x * p2.y;
              return area / 2;
            },
            // 计算多边形质心
            getPolygonAreaCenter(points) {
              let sum_x = 0;
              let sum_y = 0;
              let sum_area = 0;
              let p1 = points[1];
              for (var i = 2; i < points.length; i++) {
                let p2 = points[i];
                let area = this.Area(points[0], p1, p2);
                sum_area += area;
                sum_x += (points[0].x + p1.x + p2.x) * area;
                sum_y += (points[0].y + p1.y + p2.y) * area;
                p1 = p2;
              }
              return {
                x: sum_x / sum_area / 3,
                y: sum_y / sum_area / 3
              };
            },
            // 根据坐标信息绘制图形
            drawWithInfo() {
              this.info.forEach(item => {
                this.context.beginPath();
                if (!item.type) {
                  // 设置颜色
                  this.context.strokeStyle = item.regionColor;
                  this.context.fillStyle = item.regionColor;
                  // 绘制多边形的边
                  if (typeof item.region === "string") {
                    item.region = JSON.parse(item.region);
                  }
                  item.region.forEach(point => {
                    this.context.lineTo(point.x * this.width, point.y * this.height);
                  });
                  this.context.closePath();
                  // 在多边形质心标注文字
                  let point = this.getPolygonAreaCenter(item.region);
                  this.context.fillText(
                    item.areaName,
                    point.x * this.width,
                    point.y * this.height
                  );
                } else if (item.type === "rec") {
                  this.context.rect(
                    item.x * this.width,
                    item.y * this.height,
                    item.w * this.width,
                    item.h * this.height
                  );
                } else if (item.type === "circle") {
                  this.drawEllipse(
                    this.context,
                    (item.x + item.a) * this.width,
                    (item.y + item.b) * this.height,
                    item.a > 0 ? item.a * this.width : -item.a * this.width,
                    item.b > 0 ? item.b * this.height : -item.b * this.height
                  );
                }
                this.context.stroke();
              });
            },
            // 鼠标移动时绘制
            canvasMove(e) {
              if (this.canvasMoveUse && this.canDraw) {
                // client是基于整个页面的坐标,offset是cavas距离pictureDetail顶部以及左边的距离
                let canvasX = e.offsetX;
                let canvasY = e.offsetY;
                if (this.lineType === "rec") {
                  // 绘制矩形时恢复起始点状态再重新绘制
                  this.context.putImageData(this.beginRec.imageData, 0, 0);
                  this.context.beginPath();
                  this.context.rect(
                    this.beginRec.x,
                    this.beginRec.y,
                    canvasX - this.beginRec.x,
                    canvasY - this.beginRec.y
                  );
                  let info = this.drawInfo[this.drawInfo.length - 1];
                  info.w = canvasX / this.width - info.x;
                  info.h = canvasY / this.height - info.y;
                } else if (this.lineType === "circle") {
                  // 绘制椭圆时恢复起始点状态再重新绘制
                  this.context.putImageData(this.beginRec.imageData, 0, 0);
                  this.context.beginPath();
                  let a = (canvasX - this.beginRec.x) / 2;
                  let b = (canvasY - this.beginRec.y) / 2;
                  this.drawEllipse(
                    this.context,
                    this.beginRec.x + a,
                    this.beginRec.y + b,
                    a > 0 ? a : -a,
                    b > 0 ? b : -b
                  );
                  let info = this.drawInfo[this.drawInfo.length - 1];
                  info.a = a / this.width;
                  info.b = b / this.height;
                }
                this.context.stroke();
              }
            },
            // 绘制椭圆
            drawEllipse(context, x, y, a, b) {
              context.save();
              var r = a > b ? a : b;
              var ratioX = a / r;
              var ratioY = b / r;
              context.scale(ratioX, ratioY);
              context.beginPath();
              context.arc(x / ratioX, y / ratioY, r, 0, 2 * Math.PI, false);
              context.closePath();
              context.restore();
            },
            // 鼠标抬起
            canvasUp(e) {
              const canvas = document.getElementById(this.random);
              this.drawImg = canvas.toDataURL(); //鼠标抬起保存图片
              if (this.canDraw) {
                this.canvasMoveUse = false;
              }
              
            },
                // 获取坐标信息
            getInfo() {
              return this.drawInfo;
            },
            // 清空画布
            clean() {
              this.context.drawImage(this.img, 0, 0, this.width, this.height);
              this.drawInfo = [];
              if (this.info && this.info.length !== 0) this.drawWithInfo();
            }
        },
        beforeDestroy() {
        	this.url = "";
        }
    }