仿海康回放的时间轴线功能

3,706 阅读1分钟

前言

最近在做摄像头的视频实时监控,采用的是WebRTC流媒体转发的无插件方式,在做回放功能时需要做一块时间轴线可拖拽切换时间的功能,需要还原海康回放的时间轴线一致,下面给大家分享下实现功能的源码。

在vue中使用

这里使用了时间工具类库moment,没有的可以安装下或者自己修改源码中的时间转换

npm install moment

代码中有做相应注释

<template>
  <div class="playbackbar" id="playbackbar_div">
    <canvas id="playbackbar"></canvas>
  </div>
</template>

<script>
import moment from "moment";
export default {
  components: {},
  props: {
    // 初始化时间 => 2021-08-20 00:00:00
    initDate: {
      type: String,
    },
    /**
     * 记录的区域列表
     * @attribute startTime 开始时间 YYYY-MM-DD HH:mm:ss
     * @attribute endTime 结束时间 YYYY-MM-DD HH:mm:ss
     */
    timeData: {
      type: Array,
      default: () => [],
    },
  },
  data() {
    return {
      nowDate: "", // 当前时间
      mouseNowDate: "", // 鼠标移入画布时当前时间记录

      ctx: null, // 画布区域环境
      _width: 0, // 画布宽
      _height: 0, // 画布高

      isMouseDown: false, // 是否在画布上按下鼠标
      mousedownX: 0, // 鼠标按下的X
      mousedownY: 0, // 鼠标按下的Y
      size: 160, // 坐标尺寸
      axisTick: 1, // 默认刻度1,默认一小时一刻度
    };
  },

  watch: {
    initDate() {
      this.nowDate = this.initDate;
      this.initCanvas();
    },
  },
  computed: {
    // 计算1个移动像素代表多少秒数
    pxToSecond() {
      return (this.axisTick * 60 * 60) / this.size;
    },
  },
  mounted() {
    this.nowDate = this.initDate;
    this.initCanvas();
    this.onDocumentEvent();
    // 注册事件 -- 防抖
    let bAntiShike = false;
    window.onresize = () => {
      if (!bAntiShike) {
        bAntiShike = true;
        setTimeout(() => {
          this.initCanvas();
          bAntiShike = false;
        }, 300);
      }
    };
  },
  beforeDestroy() {
    // 事件注销
    window.onresize = null;
    document.onmousemove = null;
    document.onmouseup = null;
  },
  methods: {
    initCanvas() {
      const box = document.getElementById("playbackbar_div");
      this._width = box.offsetWidth;
      this._height = box.offsetHeight;
      const canvas = document.getElementById("playbackbar");
      canvas.width = this._width;
      canvas.height = this._height;
      this.ctx = canvas.getContext("2d");
      this.onBaseFill(this.ctx, this._width, this._height);
      this.onDrawNowDate(this.ctx, this._width);
      this.onMouseHandle(canvas, this.ctx, this._width, this._height);
    },
    // 初始化绘制
    onBaseFill(ctx, _width, _height) {
      // 时间线上轴线
      ctx.beginPath();
      ctx.strokeStyle = "#303030";
      ctx.lineWidth = 3;
      ctx.moveTo(0, 40.5);
      ctx.lineTo(_width, 40.5);
      ctx.stroke();

      this.onDrawTick(ctx, _width, _height);
      this.onDrawRecordArea(ctx, _width, _height);
      // 时间抽线区域线
      ctx.beginPath();
      ctx.strokeStyle = "#B1BE76";
      ctx.lineWidth = 1;
      ctx.moveTo(0, 42.5);
      ctx.lineTo(_width, 42.5);
      ctx.stroke();
      ctx.beginPath();
      ctx.strokeStyle = "#FFFF00";
      ctx.lineWidth = 1;
      ctx.moveTo(0, 50.5);
      ctx.lineTo(_width, 50.5);
      ctx.stroke();
      // 中间时间线
      ctx.beginPath();
      ctx.strokeStyle = "#ffcc00";
      ctx.lineWidth = 2;
      ctx.moveTo(_width / 2 - 1 + 0.5, 0);
      ctx.lineTo(_width / 2 - 1 + 0.5, _height);
      ctx.stroke();
    },
    // 轴线刻度线
    onDrawTick(ctx, _width, _height) {
      // 当前时间的毫秒数
      const nowTime = new Date(this.nowDate).getTime();
      // 离当前时间左边最近的整数点毫秒数
      const leftIntTime = new Date(
        moment(this.nowDate).format("YYYY-MM-DD HH") + ":00:00"
      ).getTime();
      const nowHour = moment(this.nowDate).format("HH");
      // 当前时间距离左侧整数时间的秒数
      const leftIntSecond = (nowTime - leftIntTime) / 1000;
      // 求出中间时间点距离左侧整数点的像素距离
      const leftIntLength = leftIntSecond / this.pxToSecond;
      // 以该整数点为中间点向左右绘制轴线
      const center = _width / 2 - 1 - leftIntLength;
      // 左侧轴线个数
      const leftTickNumber = Math.floor(center / this.size);
      // 右侧轴线个数
      const rightTickNumber = Math.floor((_width - center) / this.size);
      // 左轴线绘制
      for (let i = 0; i <= leftTickNumber; i++) {
        // 当前X轴的位置
        const _X = center - i * this.size;
        let hour = Number(nowHour) - i;
        if (hour < 0) {
          hour += 24;
        }
        const timeText = hour < 10 ? `0${hour}:00` : `${hour}:00`;
        // 绘制轴线
        ctx.beginPath();
        ctx.strokeStyle = "#4B4B4B";
        ctx.lineWidth = 2;
        ctx.moveTo(_X + 0.5, 40);
        ctx.lineTo(_X + 0.5, _height);
        ctx.stroke();
        ctx.font = "10px Arial";
        ctx.fillStyle = "#969696";
        ctx.fillText(timeText, _X + 0.5 - 12, 34);
      }
      // 右轴线绘制
      for (let i = 1; i <= rightTickNumber; i++) {
        // 当前X轴的位置
        const _X = center + i * this.size;
        let hour = Number(nowHour) + i;
        if (hour >= 24) {
          hour -= 24;
        }
        const timeText = hour < 10 ? `0${hour}:00` : `${hour}:00`;
        // 绘制轴线
        ctx.beginPath();
        ctx.strokeStyle = "#4B4B4B";
        ctx.lineWidth = 2;
        ctx.moveTo(_X + 0.5, 40);
        ctx.lineTo(_X + 0.5, _height);
        ctx.stroke();
        ctx.font = "10px Arial";
        ctx.fillStyle = "#969696";
        ctx.fillText(timeText, _X + 0.5 - 12, 34);
      }
    },
    // 绘制当前时间
    onDrawNowDate(ctx, _width) {
      ctx.font = "14px Arial";
      ctx.fillStyle = "#ffffff";
      ctx.fillText(this.nowDate, _width / 2 - 75, 24);
    },
    // 绘制存在视频记录区域
    onDrawRecordArea(ctx, _width, _height) {
      // 当前时间的毫秒数
      const nowTime = new Date(this.nowDate).getTime();
      // 左侧顶点的时间秒数
      const leftVertexTime = nowTime / 1000 - (_width / 2) * this.pxToSecond;
      // 右侧顶点的时间秒数
      const rightVertexTime = nowTime / 1000 + (_width / 2) * this.pxToSecond;
      // 在可视范围内的视频记录
      const timeList = this.timeData.filter((r) => {
        const startTimeSecond = new Date(r.startTime).getTime() / 1000;
        const endTimeSecond = new Date(r.endTime).getTime() / 1000;
        if (
          (startTimeSecond < leftVertexTime &&
            endTimeSecond > leftVertexTime) ||
          (startTimeSecond >= leftVertexTime &&
            startTimeSecond < rightVertexTime)
        ) {
          return true;
        } else {
          return false;
        }
      });
      timeList.map((r) => {
        const startTimeSecond = new Date(r.startTime).getTime() / 1000;
        const endTimeSecond = new Date(r.endTime).getTime() / 1000;
        //如果起始时间比左侧顶点时间小,说明区域从左侧顶点开始
        if (startTimeSecond < leftVertexTime) {
          // 获取该时间段在可视范围的长度
          const _W =
            endTimeSecond < rightVertexTime
              ? (endTimeSecond - leftVertexTime) / this.pxToSecond
              : _width;

          // 绘制播放长度 矩形区域
          ctx.beginPath();
          ctx.fillStyle = "#637DEC";
          ctx.fillRect(0, 43, _W, 8);
        }
        // 如果起始时间在可视范围内
        if (startTimeSecond >= leftVertexTime) {
          // 算出起始时间的X位置
          const _X = (startTimeSecond - leftVertexTime) / this.pxToSecond;
          const _W = (endTimeSecond - startTimeSecond) / this.pxToSecond; // 记录长度
          // 绘制播放长度 矩形区域
          ctx.beginPath();
          ctx.fillStyle = "#637DEC";
          ctx.fillRect(_X, 43, _W, 8);
        }
      });
    },
    // 鼠标事件
    onMouseHandle(canvas, ctx, _width, _height) {
      // 鼠标移入
      canvas.onmouseover = (e) => {
        // 移入时记录当前时间,在鼠标移动时不会根据当前时间变化而变化
        if (!this.isMouseDown) {
          this.mouseNowDate = this.nowDate;
        }
      };
      // 鼠标移出
      canvas.onmouseout = (e) => {
        ctx.clearRect(0, 0, _width, _height);
        this.onBaseFill(ctx, _width, _height);
        this.onDrawNowDate(ctx, _width);
      };
      // 鼠标移动
      canvas.onmousemove = (e) => {
        if (!this.isMouseDown) {
          ctx.clearRect(0, 0, _width, _height);
          this.onBaseFill(ctx, _width, _height);
          const offsetX = e.offsetX; // 鼠标在画布的X轴位置
          // 鼠标移动的距离, 画布中间为起点
          const moveLength = offsetX - _width / 2;
          let nowDateTime = new Date(this.mouseNowDate).getTime();
          const nowDate = moment(
            nowDateTime + this.pxToSecond * moveLength * 1000
          ).format("YYYY-MM-DD HH:mm:ss");
          ctx.font = "12px Arial";
          ctx.fillStyle = "#ffffff";
          ctx.fillText(nowDate, offsetX - 62, 12);
          this.onDrawNowDate(ctx, _width);
        }
      };
      // 鼠标按下
      canvas.onmousedown = (e) => {
        this.mousedownX = e.clientX;
        this.mousedownY = e.clientY;
        this.isMouseDown = true;
      };
    },
    // 全局事件
    onDocumentEvent() {
      // 全局鼠标抬起
      document.onmouseup = (e) => {
        // 只有在画布中按下鼠标才生效
        if (this.isMouseDown) {
          this.mouseNowDate = this.nowDate; // 鼠标抬起时获取到最终的当前时间
          this.isMouseDown = false;
        }
      };
      // 全局鼠标移动
      document.onmousemove = (e) => {
        // 只有在画布中按下鼠标才生效
        if (this.isMouseDown) {
          this.ctx.clearRect(0, 0, this._width, this._height);
          this.onBaseFill(this.ctx, this._width, this._height);
          const clientX = e.clientX; // 鼠标在可视窗口的X轴位置
          let nowDateTime = new Date(this.mouseNowDate).getTime();
          // 画布移动的距离, 鼠标按下为起点
          const moveLength = this.mousedownX - clientX;
          this.nowDate = moment(
            nowDateTime + this.pxToSecond * moveLength * 1000
          ).format("YYYY-MM-DD HH:mm:ss");
          this.onDrawNowDate(this.ctx, this._width);
        }
      };
    },
  },
};
</script>
<style lang="scss" scoped>
.playbackbar {
  width: 100%;
  height: 65px;
  background-color: #343434;
  canvas {
    cursor: pointer;
    background: #000;
  }
}
</style>

效果图

image.png

有更好优化意见可以打在评论区,这里只做为功能实现的源码分享