前端实现时间轴组件拼接N多个不连续监控视频展示

这两天心情不美丽,事儿赶事儿让人心情不爽。

不过再不爽钱还得挣!

需求

接到一个需求:

  1. 服务器上有大量的不连贯监控视频,需要整合到一个时间轴上显示。
  2. 后台向前端推送监控视频的起止时间和地址。
  3. 拖动时间轴到某个视频上则开始播放这个视频。
  4. 拖动时间轴到某个视频的某个时间点,则视频从某个时间点开始播放。(类似于视频进度条)

实现思路

  • 因为后端向前端推送视频的起止时间和地址,所以前端不必再操心数据部分了。
  • 现在需要在时间轴上显示哪个时间上有视频,通过色块进行区分。
  • 当时间轴拖动到某个时间段的时候,判断当前时间段是否有监控视频,如果有则播放。
  • 这里的时间轴既是时间轴也是进度条,所以这里可能有和video部分的联动。

实现方案

考虑还是使用现成的插件加上二次开发的方式实现,时间轴使用vis-timeline插件实现,视频播放方面还是用比较熟悉的video.js实现。

安装插件

bash 复制代码
npm i dayjs moment video.js vis-timeline vis-data

这里需要注意,尽量还是dayjsmoment都安装一下。

dayjs的主要作用是把日期统一到一个维度上来。

moment的主要作用是对vis-timeline进行国际化。

另外新版的vis-timeline好像已经将DataSet内置了,不再需要额外安装vis-data

我这里是因为使用了Vue2的原因,需要大家自行验证一下Vue3

Video实现

html 复制代码
<video id="video-player" ref="videoPlayer" class="video-js vjs-default-skin" controls preload="auto"></video>

<script>
    export default {
        mounted() {
            this.initPlayer()
        },
        methods: {
            // 可以在这里额外绑定播放器和时间轴的进度关系,达到一个双向进度数据绑定的效果。
            initPlayer() {
                let that = this;
                this.videoPlayer = videojs(this.$refs.videoPlayer, {
                    fluid: true,
                    autoplay: false,
                    controls: true
                });

                this.videoPlayer.on('play', function () {
                    that.isPlay = true;
                });
                this.videoPlayer.on('pause', function () {
                    that.isPlay = false;
                });
                this.videoPlayer.on('ended', function () {
                    that.isPlay = false;
                });
            },
        }
    }
</script>

时间轴实现

这里一定要注意,给组件一个确切的宽高。

Ps: 时间轴上展示内容区间的色块容易与时间轴对不齐,当给定宽高以后无异常。

html 复制代码
<div class="video-player-timeline">
    <div class="timeline"></div>
    <div id="visualization"></div>
</div>

<script>
export default {
    data() {
        return {
            timeline: null,
            currentTime: null, // 当前播放时间
            isPlaying: false,  // 播放状态
            timer: null,       // 定时器
            centerTime: '',    // 中心时间显示
            videoPlayer: null,
            isFirst: true,    // 是否是第一次播放
            isPlay: false,  // 播放状态
            isVideo: true,   // 是否有视频
        }
    },
    methods: {
        initTimeline() {
            // 处理传入的segments,确保时间正确解析
            const processedSegments = this.segments.map(segment => {
                return {
                    ...segment,
                    start: dayjs(segment.start).toISOString(),
                    end: dayjs(segment.end).toISOString()
                };
            });

            var container = document.getElementById('visualization');
            this.timeline = new Timeline(container, new DataSet(processedSegments), {
                locale: 'zh-cn',
                moment: (date) => moment(date).locale('zh-cn')
            });

            // 绑定时间轴范围变化事件
            this.timeline.on('rangechanged', () => {
                this.updateCenterTime();
            });
        },
        // 更新中心时间显示
        updateCenterTime() {
            if (!this.timeline) return;

            const window = this.timeline.getWindow();
            const centerTimestamp = (window.start.getTime() + window.end.getTime()) / 2;
            const centerDate = new Date(centerTimestamp);

            // 格式化时间为 YYYY-MM-DD HH:mm:ss
            const year = centerDate.getFullYear();
            const month = String(centerDate.getMonth() + 1).padStart(2, '0');
            const day = String(centerDate.getDate()).padStart(2, '0');
            const hours = String(centerDate.getHours()).padStart(2, '0');
            const minutes = String(centerDate.getMinutes()).padStart(2, '0');
            const seconds = String(centerDate.getSeconds()).padStart(2, '0');

            this.centerTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;

            // 更新中心时间以后,设置video播放哪个视频
            if (this.videoPlayer) {
                this.setActiveSegment();
            }
        },
        // 设置时间轴中心点与指定时间重合
        setCenterTime(centerTime) {
            const targetTime = dayjs(centerTime);

            // 获取当前时间轴窗口的持续时间(毫秒)
            const window = this.timeline.getWindow();
            const windowDuration = window.end.getTime() - window.start.getTime();

            // 计算新的开始和结束时间,使目标时间位于中心
            const newStart = new Date(targetTime.getTime() - windowDuration / 2);
            const newEnd = new Date(targetTime.getTime() + windowDuration / 2);

            // 设置时间轴窗口
            this.timeline.setWindow(newStart, newEnd);
        },
        // 设置播放哪个视频
        setActiveSegment() {
            const activeSegment = this.getActiveSegment();
            if (!activeSegment) return;
            // 设置视频源
            this.videoPlayer.src({
                type: 'video/mp4',
                src: activeSegment.url
            });
            // 获取当前视频从第几秒开始播放
            let currentTime = dayjs(this.centerTime);
            let activeSegmentStart = dayjs(activeSegment.start);
            let duration = currentTime.diff(activeSegmentStart, 'second');
            this.videoPlayer.currentTime(duration);
            if (!this.isFirst) {
                // 播放视频
                this.videoPlayer.play();
            }
            this.isFirst = false;
        },
        // 获取当前时间对应的视频
        getActiveSegment() {
            // 如果没有传入segments,则返回null
            if (!this.segments || this.segments.length === 0) return null;
            let timer = dayjs(this.centerTime).toISOString();
            let findVideo = this.segments.find(seg => {
                const s = dayjs(seg.start).toISOString();
                const e = dayjs(seg.end).toISOString();
                return timer >= s && timer <= e;
            }) || null;
            this.isVideo = !!findVideo;
            return findVideo;
        },
    }
}
</script>

总结

其实这种交互在监控视频上看还是相当友好的,不但符合一般用户的使用习惯,而且在大多数的监控类软件中都有类似的方案存在。

比如米家App中,查看监控回放的时候,方案就与这个类似。

不过大家需要注意,尽量让客户减少缩放时间轴的操作,甚至直接禁用缩放的操作。因为缩放会导致时间轴上选中视频不断变化。

这个组件还可以不断的深入拓展,应用到直播方面也可以。类似于将直播和回放两种功能结合在一起了。

相关推荐
岁月向前2 小时前
iOS UI基础和内存管理相关
前端
Magicman3 小时前
JS筑基(二)-关于this指向
前端
Asort3 小时前
精通React JSX:高级开发者必备的语法规则与逻辑处理技巧
前端·javascript·react.js
Mintopia3 小时前
想摸鱼背单词?我用 Cursor 一个小时开发了一个 Electron 应用
前端·javascript·cursor
JarvanMo3 小时前
Flutter PruneKit - 从你的Flutter代码中干掉那些已经死掉的代码
前端
500佰3 小时前
最近做产品开发,总结出一些通病
前端
serve the people3 小时前
Formatting Outputs for ChatPrompt Templates(two)
前端·数据库
小皮虾3 小时前
魔法降临!让小程序调用云函数如丝般顺滑,调用接口仿佛就是调用存在于本地的函数
前端·微信小程序·小程序·云开发
StarkCoder3 小时前
Flutter微任务解析:如何解决原生线程回调导致的UI状态异常
前端