vue3+ts实现视频根据时间轴截取,并可以通过传入截取起止时间进行当前剪辑的回显
转载 https://blog.csdn.net/wed2019/article/details/126995825?spm=1001.2014.3001.5502
公司提出想做一个视频编辑功能,每次只裁剪一段即可,UI同时也想实现时间轴为关键帧图片的效果,从网上也没找到合适的组件,简单思考后觉得并不难,决定自己封装一个吧。组件涉及到的只有vue3+ts+scss,没有使用其他插件。
穿插一个简化版本,时间轴是一条线,功能比这个简化,或许会符合部分人的需求。
功能概述
通过传入源视频时长,源视频的视频地址,当前剪辑的开始时间,当前剪辑的结束时间和关键帧缩略图(需要20张图片,后端提供,根据视频时长分为20节,每节取一张图)五个必传参数,视频地址将通过video标签播放,组件尺寸为100%,根据父级组件的宽度自动撑满。
时间轴模块,会根据传入的起止时间自动换算出1px===毫秒数,起止时间间隔我设置了1秒以上,开始时间拖动到结束时间前一秒左右将停止移动,结束时间拖动到开始时间后一秒左右将无法拖动,拖动开始时间时会自动将video标签的开始播放时间定位到截取的开始时间,设置结束时间后,video播放到截取的结束时间后会自动暂停,这时video标签将只能播放所截取的起止时间范围的视频。最后设置了回调queryTime(),通过回调将起止时间传出,我的业务中视频截取是后端操作,前端只需要提供截取的起止时间即可,具体看代码,如下:
参数描述
endTime 视频结束时间,精确到毫秒
url 视频地址,将通过video标签展示
spliterStartTime 视频截取开始时间
spliterEndTime 视频截取结束时间
photoList 时间轴缩略图列表
回调描述
回调方法 回调参数(形参) 参数描述
queryTime Array [开始时间,结束时间]
template部分
<template> <video id="videoPlayer" @play="onplay" controls="true" preload="auto" muted class="video" width="100%" :src="props.url"></video> <ul class="time-list"> <li v-for="item in data.timeList" :key="item">{{item}}</li> </ul> <div class="crop-filter"> <div class="timer-shaft" ref="shaft"> <div class="white-shade" :style="{width:(data.endLeft-data.startLeft+12)+'px',left:data.startLeft-6+'px'}"> </div> <div class="left-shade" :style="{width: (data.startLeft-6)+'px'}"></div> <div class="right-shade" :style="{width: (shaft?.clientWidth-data.endLeft-6) +'px'}"></div> <div class="strat-circle circle" ref="start" @mousedown="startMouseDown"> <div class="center"></div> </div> <div class="end-circle circle" ref="end" @mousedown="endMouseDown"> <div class="center"></div> </div> <!-- 此处src应绑定item --> <img @dragstart.prevent style="width: 5%;user-select: none;" v-for="item in props.photoList" src="../../../public/favicon.ico" alt=""> </div> </div> </template>
分为三个部分,上面是video标签,中间是根据总时长处理出的时间数组,下面是时间轴。
script部分
<!-- 起止时间间隔最小≈1秒 --> <script setup lang="ts"> import { getNowTime, dateStrChangeTimeTamp, cropFilter, videoRef, } from '@/types/type' // 进度条dom const shaft = ref(null); // 开始按钮dom const start = ref(null); // 结束按钮dom const end = ref(null); const data = reactive(new cropFilter) // props参数类型 interface Props { startTime ? : string; endTime: string; url: string; spliterStartTime ? : string; spliterEndTime: string; // 此处为模拟 photoList: string[]; } // 设置默认值,需要显式的开启,具体查看vue3文档 const props = withDefaults(defineProps < Props > (), { startTime: '00:00:00.0', endTime: '00:00:08.0', spliterStartTime: '00:00:00.0', spliterEndTime: '00:00:08.0', url: '', photoList: [], }) const emit = defineEmits(['queryTime']) onMounted(() => { // 随便拼一个1970年以后的年月日字符串+' ' let str = '1970-01-02 ' let time = dateStrChangeTimeTamp(str + props.endTime) - dateStrChangeTimeTamp(str + props.startTime) data.roal = time / shaft.value.clientWidth // 结束毫秒数 let endM = (dateStrChangeTimeTamp('1970-01-02 ' + props?.spliterEndTime) - (1000 * 60 * 60 * 16)) // 开始毫秒数 let startM = (dateStrChangeTimeTamp('1970-01-02 ' + (props?.spliterStartTime)) - (1000 * 60 * 60 * 16)) console.log(startM, endM) // 设置开始结束位置 start.value.style.left = startM / data.roal - (end.value.clientWidth / 2) + 'px' end.value.style.left = endM / data.roal - (end.value.clientWidth / 2) + 'px' data.endLeft = end.value.offsetLeft data.endright = shaft.value.clientWidth - (end.value.clientWidth / 2) data.startLeft = start.value.offsetLeft + (start.value.clientWidth / 2) getVideoTime() data.timeList.push(props.startTime) let paragraph = (dateStrChangeTimeTamp(str + props.endTime) - (1000 * 60 * 60 * 16)) / 5 for (let i = 1; i < 6; i++) { data.timeList.push(getNowTime(paragraph * i)) } }) // 播放事件 const onplay = () => { let myVideo: videoRef = document.getElementById('videoPlayer'); // 开始秒数 let startM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.startTime ? data.startTime : props .spliterStartTime)) - (1000 * 60 * 60 * 16)) / 1000 // 结束秒数 let endM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.endTime ? data.endTime : props .spliterEndTime)) - (1000 * 60 * 60 * 16)) / 1000 // 如果当前秒数小于等于截取的开始时间,就按截取的开始时间播放,如果不是,则为继续播放 if (myVideo.currentTime <= startM || myVideo.currentTime > endM) { myVideo.currentTime = startM; myVideo.play(); } } // 获取视频播放时长 const getVideoTime = () => { if (document.getElementById('videoPlayer')) { let videoPlayer: videoRef = document.getElementById('videoPlayer'); videoPlayer.addEventListener('timeupdate', function() { // 结束秒数 let endM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.endTime ? data.endTime : props .spliterEndTime)) - (1000 * 60 * 60 * 16)) / 1000 // 如果当前播放时间大于等于截取的结束秒数,就暂停 if (videoPlayer.currentTime >= endM) { videoPlayer.pause() } }, false) } } //设置播放点 const playBySeconds = (num: number) => { if (num && document.getElementById('videoPlayer')) { let myVideo: videoRef = document.getElementById('videoPlayer'); myVideo.currentTime = num; } } // 起始按钮 const startMouseDown = (e) => { let odiv = e.currentTarget; //获取目标父元素 //算出鼠标相对元素的位置 let disX = e.clientX - odiv.offsetLeft; document.onmousemove = (e) => { //鼠标按下并移动的事件 //用鼠标的位置减去鼠标相对元素的位置,得到元素的位置 let left = e.clientX - disX; //移动当前元素 odiv.style.left = left + 'px'; //获取距离窗口宽度 let mas = odiv.offsetLeft; if (mas <= -(start.value.clientWidth / 2)) { odiv.style.left = -(start.value.clientWidth / 2) + 'px'; } else if (mas >= (data.endLeft - Math.ceil(1000 / data.roal))) { odiv.style.left = (data.endLeft - Math.ceil(1000 / data.roal)) + 'px'; } data.startTime = getNowTime(data.roal * Math.floor(start.value.offsetLeft + (start.value.clientWidth / 2))) data.startLeft = start.value.clientWidth + start.value.offsetLeft // 开始秒数 let startM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.startTime ? data.startTime : props .spliterStartTime)) - (1000 * 60 * 60 * 16)) / 1000 playBySeconds(startM) }; document.onmouseup = (e) => { document.onmousemove = null; document.onmouseup = null; handleTime() }; } // 结束按钮 const endMouseDown = (e) => { let odiv = e.currentTarget; //获取目标父元素 //算出鼠标相对元素的位置 let disX = e.clientX - odiv.offsetLeft; document.onmousemove = (e) => { //鼠标按下并移动的事件 //用鼠标的位置减去鼠标相对元素的位置,得到元素的位置 let left = e.clientX - disX; //移动当前元素 odiv.style.left = left + 'px'; //获取距离窗口宽度 let mas = odiv.offsetLeft; if (mas <= (data.startLeft - end.value.clientWidth + Math.ceil(1000 / data.roal))) { odiv.style.left = (data.startLeft - end.value.clientWidth + Math.ceil(1000 / data.roal)) + 'px'; } else if (mas >= data.endright) { odiv.style.left = data.endright + 'px'; } data.endTime = getNowTime(data.roal * Math.floor(end.value.offsetLeft + (end.value.clientWidth / 2))) data.endLeft = end.value.offsetLeft }; document.onmouseup = (e) => { document.onmousemove = null; document.onmouseup = null; handleTime() }; } // 传出起止时间的回调 const handleTime = () => { let arr = [data.startTime, data.endTime] emit('queryTime', arr) } </script>
css部分
<style scoped lang="scss"> .video { width: 100%; margin-bottom: 0.2rem; } .time-list { width: 100%; color: #C0C0C0; font-size: 0.12rem; margin-bottom: 0.1rem; display: flex; align-items: center; justify-content: space-between; } .crop-filter { width: 100%; padding: 0 0.1rem; box-sizing: border-box; display: flex; align-items: center; .timer-shaft { width: 100%; position: relative; .circle { width: 0.2rem; position: absolute; top: -8%; height: 110%; background-color: #ffffff; cursor: e-resize; display: flex; align-items: center; justify-content: center; .center { width: 0.02rem; height: 0.15rem; background-color: #D8D8D8; } } .strat-circle { left: -0.09rem; border-radius: 0.03rem 0 0 0.03rem; } .end-circle { right: -0.1rem; border-radius: 0 0.03rem 0.03rem 0; } .white-shade { position: absolute; top: -8%; height: 110%; width: 100%; background-color: transparent; border: 0.04rem solid #fff; box-sizing: border-box; border-left: 0; border-right: 0; } .left-shade { position: absolute; left: 0; top: 0; height: 100%; background: rgba(0, 0, 0, 0.6); } .right-shade { position: absolute; right: 0; top: 0; height: 100%; background: rgba(0, 0, 0, 0.6); } } } </style>
.vue完整代码
<template> <video id="videoPlayer" @play="onplay" controls="true" preload="auto" muted class="video" width="100%" :src="props.url"></video> <ul class="time-list"> <li v-for="item in data.timeList" :key="item">{{item}}</li> </ul> <div class="crop-filter"> <div class="timer-shaft" ref="shaft"> <div class="white-shade" :style="{width:(data.endLeft-data.startLeft+12)+'px',left:data.startLeft-6+'px'}"> </div> <div class="left-shade" :style="{width: (data.startLeft-6)+'px'}"></div> <div class="right-shade" :style="{width: (shaft?.clientWidth-data.endLeft-6) +'px'}"></div> <div class="strat-circle circle" ref="start" @mousedown="startMouseDown"> <div class="center"></div> </div> <div class="end-circle circle" ref="end" @mousedown="endMouseDown"> <div class="center"></div> </div> <!-- 此处src应绑定item --> <img @dragstart.prevent style="width: 5%;user-select: none;" v-for="item in props.photoList" src="../../../public/favicon.ico" alt=""> </div> </div> </template> <!-- 起止时间间隔最小≈1秒 --> <script setup lang="ts"> import { getNowTime, dateStrChangeTimeTamp, cropFilter, videoRef, } from '@/types/type' // 进度条dom const shaft = ref(null); // 开始按钮dom const start = ref(null); // 结束按钮dom const end = ref(null); const data = reactive(new cropFilter) // props参数类型 interface Props { startTime ? : string; endTime: string; url: string; spliterStartTime ? : string; spliterEndTime: string; // 此处为模拟 photoList: string[]; } // 设置默认值,需要显式的开启,具体查看vue3文档 const props = withDefaults(defineProps < Props > (), { startTime: '00:00:00.0', endTime: '00:00:08.0', spliterStartTime: '00:00:00.0', spliterEndTime: '00:00:08.0', url: '', photoList: [], }) const emit = defineEmits(['queryTime']) onMounted(() => { // 随便拼一个1970年以后的年月日字符串+' ' let str = '1970-01-02 ' let time = dateStrChangeTimeTamp(str + props.endTime) - dateStrChangeTimeTamp(str + props.startTime) data.roal = time / shaft.value.clientWidth // 结束毫秒数 let endM = (dateStrChangeTimeTamp('1970-01-02 ' + props?.spliterEndTime) - (1000 * 60 * 60 * 16)) // 开始毫秒数 let startM = (dateStrChangeTimeTamp('1970-01-02 ' + (props?.spliterStartTime)) - (1000 * 60 * 60 * 16)) console.log(startM, endM) // 设置开始结束位置 start.value.style.left = startM / data.roal - (end.value.clientWidth / 2) + 'px' end.value.style.left = endM / data.roal - (end.value.clientWidth / 2) + 'px' data.endLeft = end.value.offsetLeft data.endright = shaft.value.clientWidth - (end.value.clientWidth / 2) data.startLeft = start.value.offsetLeft + (start.value.clientWidth / 2) getVideoTime() data.timeList.push(props.startTime) let paragraph = (dateStrChangeTimeTamp(str + props.endTime) - (1000 * 60 * 60 * 16)) / 5 for (let i = 1; i < 6; i++) { data.timeList.push(getNowTime(paragraph * i)) } }) // 播放事件 const onplay = () => { let myVideo: videoRef = document.getElementById('videoPlayer'); // 开始秒数 let startM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.startTime ? data.startTime : props .spliterStartTime)) - (1000 * 60 * 60 * 16)) / 1000 // 结束秒数 let endM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.endTime ? data.endTime : props .spliterEndTime)) - (1000 * 60 * 60 * 16)) / 1000 // 如果当前秒数小于等于截取的开始时间,就按截取的开始时间播放,如果不是,则为继续播放 if (myVideo.currentTime <= startM || myVideo.currentTime > endM) { myVideo.currentTime = startM; myVideo.play(); } } // 获取视频播放时长 const getVideoTime = () => { if (document.getElementById('videoPlayer')) { let videoPlayer: videoRef = document.getElementById('videoPlayer'); videoPlayer.addEventListener('timeupdate', function() { // 结束秒数 let endM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.endTime ? data.endTime : props .spliterEndTime)) - (1000 * 60 * 60 * 16)) / 1000 // 如果当前播放时间大于等于截取的结束秒数,就暂停 if (videoPlayer.currentTime >= endM) { videoPlayer.pause() } }, false) } } //设置播放点 const playBySeconds = (num: number) => { if (num && document.getElementById('videoPlayer')) { let myVideo: videoRef = document.getElementById('videoPlayer'); myVideo.currentTime = num; } } // 起始按钮 const startMouseDown = (e) => { let odiv = e.currentTarget; //获取目标父元素 //算出鼠标相对元素的位置 let disX = e.clientX - odiv.offsetLeft; document.onmousemove = (e) => { //鼠标按下并移动的事件 //用鼠标的位置减去鼠标相对元素的位置,得到元素的位置 let left = e.clientX - disX; //移动当前元素 odiv.style.left = left + 'px'; //获取距离窗口宽度 let mas = odiv.offsetLeft; if (mas <= -(start.value.clientWidth / 2)) { odiv.style.left = -(start.value.clientWidth / 2) + 'px'; } else if (mas >= (data.endLeft - Math.ceil(1000 / data.roal))) { odiv.style.left = (data.endLeft - Math.ceil(1000 / data.roal)) + 'px'; } data.startTime = getNowTime(data.roal * Math.floor(start.value.offsetLeft + (start.value.clientWidth / 2))) data.startLeft = start.value.clientWidth + start.value.offsetLeft // 开始秒数 let startM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.startTime ? data.startTime : props .spliterStartTime)) - (1000 * 60 * 60 * 16)) / 1000 playBySeconds(startM) }; document.onmouseup = (e) => { document.onmousemove = null; document.onmouseup = null; handleTime() }; } // 结束按钮 const endMouseDown = (e) => { let odiv = e.currentTarget; //获取目标父元素 //算出鼠标相对元素的位置 let disX = e.clientX - odiv.offsetLeft; document.onmousemove = (e) => { //鼠标按下并移动的事件 //用鼠标的位置减去鼠标相对元素的位置,得到元素的位置 let left = e.clientX - disX; //移动当前元素 odiv.style.left = left + 'px'; //获取距离窗口宽度 let mas = odiv.offsetLeft; if (mas <= (data.startLeft - end.value.clientWidth + Math.ceil(1000 / data.roal))) { odiv.style.left = (data.startLeft - end.value.clientWidth + Math.ceil(1000 / data.roal)) + 'px'; } else if (mas >= data.endright) { odiv.style.left = data.endright + 'px'; } data.endTime = getNowTime(data.roal * Math.floor(end.value.offsetLeft + (end.value.clientWidth / 2))) data.endLeft = end.value.offsetLeft }; document.onmouseup = (e) => { document.onmousemove = null; document.onmouseup = null; handleTime() }; } // 传出起止时间的回调 const handleTime = () => { let arr = [data.startTime, data.endTime] emit('queryTime', arr) } </script> <style scoped lang="scss"> .video { width: 100%; margin-bottom: 0.2rem; } .time-list { width: 100%; color: #C0C0C0; font-size: 0.12rem; margin-bottom: 0.1rem; display: flex; align-items: center; justify-content: space-between; } .crop-filter { width: 100%; padding: 0 0.1rem; box-sizing: border-box; display: flex; align-items: center; .timer-shaft { width: 100%; position: relative; .circle { width: 0.2rem; position: absolute; top: -8%; height: 110%; background-color: #ffffff; cursor: e-resize; display: flex; align-items: center; justify-content: center; .center { width: 0.02rem; height: 0.15rem; background-color: #D8D8D8; } } .strat-circle { left: -0.09rem; border-radius: 0.03rem 0 0 0.03rem; } .end-circle { right: -0.1rem; border-radius: 0 0.03rem 0.03rem 0; } .white-shade { position: absolute; top: -8%; height: 110%; width: 100%; background-color: transparent; border: 0.04rem solid #fff; box-sizing: border-box; border-left: 0; border-right: 0; } .left-shade { position: absolute; left: 0; top: 0; height: 100%; background: rgba(0, 0, 0, 0.6); } .right-shade { position: absolute; right: 0; top: 0; height: 100%; background: rgba(0, 0, 0, 0.6); } } } </style>
type.ts代码
export interface videoRef { // 其他冗余字段 [propName: string]: any; // 数字值,表示当前播放的时间,以秒计 currentTime: number; } export class cropFilter { // 结束按钮距离左侧距离 endLeft: string | number = 0; // 结束按钮初始位置 endright: string | number = 0; // 开始按钮距离左侧距离 startLeft: string | number = 0; // 毫秒/px(1px===的毫秒数) roal: string | number = 0; // 开始时间 startTime: string | number = 0; // 结束时间 endTime: string | number = 0; // 时间轴显示时间数组 timeList: string[] = []; } //日期字符串转成时间戳 export function dateStrChangeTimeTamp(dateStr: string) { dateStr = dateStr.substring(0, 23); dateStr = dateStr.replace(/-/g, '/'); let timeTamp = new Date(dateStr).getTime(); return timeTamp } // 精准到毫秒 export function getNowTime(val: string | number) { const date = new Date(val) const hour = (date.getHours() - 8) < 10 ? '0' + (date.getHours() - 8) : date.getHours() - 8 const minute = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes() const second = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds() const milliSeconds = date.getMilliseconds() //毫秒 const currentTime = hour + ':' + minute + ':' + second + '.' + milliSeconds console.log(currentTime) return currentTime }
————————————————
版权声明:本文为CSDN博主「一个人的咖啡~」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/wed2019/article/details/126995825