赞
踩
公司提出想做一个视频编辑功能,每次只裁剪一段即可,UI同时也想实现时间轴为关键帧图片的效果,从网上也没找到合适的组件,简单思考后觉得并不难,决定自己封装一个吧。组件涉及到的只有vue3+ts+scss,没有使用其他插件。
链接vue3+ts实现视频根据时间轴截取_vue项目截取一段正在播放的视频_一个人的咖啡~的博客-CSDN博客
通过传入源视频时长,源视频的视频地址,当前剪辑的开始时间,当前剪辑的结束时间和关键帧缩略图(需要20张图片,后端提供,根据视频时长分为20节,每节取一张图)五个必传参数,视频地址将通过video标签播放,组件尺寸为100%,根据父级组件的宽度自动撑满。
时间轴模块,会根据传入的起止时间自动换算出1px===毫秒数,起止时间间隔我设置了1秒以上,开始时间拖动到结束时间前一秒左右将停止移动,结束时间拖动到开始时间后一秒左右将无法拖动,拖动开始时间时会自动将video标签的开始播放时间定位到截取的开始时间,设置结束时间后,video播放到截取的结束时间后会自动暂停,这时video标签将只能播放所截取的起止时间范围的视频。最后设置了回调queryTime(),通过回调将起止时间传出,我的业务中视频截取是后端操作,前端只需要提供截取的起止时间即可,具体看代码,如下:
endTime | 视频结束时间,精确到毫秒 |
url | 视频地址,将通过video标签展示 |
spliterStartTime | 视频截取开始时间 |
spliterEndTime | 视频截取结束时间 |
photoList | 时间轴缩略图列表 |
回调方法 | 回调参数(形参) | 参数描述 |
queryTime | Array | [开始时间,结束时间] |
- <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标签,中间是根据总时长处理出的时间数组,下面是时间轴。
- <!-- 起止时间间隔最小≈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>
- <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>
- 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
- }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。