这两天心情不美丽,事儿赶事儿让人心情不爽。
不过再不爽钱还得挣!
需求

接到一个需求:
- 服务器上有大量的不连贯监控视频,需要整合到一个时间轴上显示。
- 后台向前端推送监控视频的起止时间和地址。
- 拖动时间轴到某个视频上则开始播放这个视频。
- 拖动时间轴到某个视频的某个时间点,则视频从某个时间点开始播放。(类似于视频进度条)
实现思路
- 因为后端向前端推送视频的起止时间和地址,所以前端不必再操心数据部分了。
- 现在需要在时间轴上显示哪个时间上有视频,通过色块进行区分。
- 当时间轴拖动到某个时间段的时候,判断当前时间段是否有监控视频,如果有则播放。
- 这里的时间轴既是时间轴也是进度条,所以这里可能有和video部分的联动。
实现方案
考虑还是使用现成的插件加上二次开发的方式实现,时间轴使用vis-timeline插件实现,视频播放方面还是用比较熟悉的video.js实现。
安装插件
bash
npm i dayjs moment video.js vis-timeline vis-data
这里需要注意,尽量还是dayjs和moment都安装一下。
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中,查看监控回放的时候,方案就与这个类似。
不过大家需要注意,尽量让客户减少缩放时间轴的操作,甚至直接禁用缩放的操作。因为缩放会导致时间轴上选中视频不断变化。
这个组件还可以不断的深入拓展,应用到直播方面也可以。类似于将直播和回放两种功能结合在一起了。