vue3+ts实现视频根据时间轴截取

公司提出想做一个视频编辑功能,每次只裁剪一段即可,从网上也没找到合适的组件,简单思考后觉得并不难,决定自己封装一个吧。组件涉及到的只有vue3+ts+scss,没有使用其他插件。

基于目前这版升级版本,可以实现时间轴为关键帧图片列表,并拖动截取,选中的地方高亮,未选中的变灰,并可以通过传入截取起止时间回显上次裁剪位置。

链接https://blog.csdn.net/wed2019/article/details/126995825

功能概述

通过传入开始时间,结束时间,视频地址三个必传参数,视频地址将通过video标签播放,组件尺寸为100%,根据父级组件的宽度自动撑满。

时间轴模块,会根据传入的起止时间自动换算出1px===毫秒数,起止时间间隔我设置了1秒以上,开始时间拖动到结束时间前一秒左右将停止移动,结束时间拖动到开始时间后一秒左右将无法拖动,设置开始时间后会自动将video标签的开始播放时间定位到截取的开始时间,设置结束时间后,video播放到截取的结束时间后会自动暂停,这时video标签将只能播放所截取的起止时间范围的视频。最后设置了回调queryTime(),通过回调将起止时间传出,我的业务中视频截取是后端操作,前端只需要提供起止时间即可,具体看代码,如下:

参数描述
startTime视频开始时间,精确到毫秒(如:00:00:00.0)
endTime视频结束时间,精确到毫秒
url视频地址,将通过video标签展示

回调描述
回调方法回调参数(形参)参数描述
queryTimeArray[开始时间,结束时间]

template部分

<template>
	<video id="videoPlayer" @play="onplay" controls="true" preload="auto" muted class="video" width="100%"
		:src="props.url"></video>
	<div class="crop-filter">
		<div class="timer-shaft" ref="shaft">
			<div class="strat-circle circle" ref="start" @mousedown="startMouseDown">
				<div class="timer">
					{{ data.startTime||props.startTime}}
				</div>
			</div>
			<div class="end-circle circle" ref="end" @mousedown="endMouseDown">
				<div class="timer">
					{{data.endTime||props.endTime}}
				</div>
			</div>
		</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;
	}
	// 设置默认值,需要显式的开启,具体查看vue3文档
	const props = withDefaults(defineProps < Props > (), {
		startTime: '00:00:00.0',
		endTime: '00:00:08.0',
		url: '',
	})
	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
		getNowTime(time)
		data.endLeft = end.value.offsetLeft
		data.endright = end.value.offsetLeft
		data.startLeft = start.value.clientWidth - (start.value.clientWidth / 2)
		getVideoTime()
	})
	// 播放事件
	const onplay = () => {
		let myVideo: videoRef = document.getElementById('videoPlayer');
		// 开始秒数
		let startM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.startTime ? data.startTime : props
			.startTime)) - (1000 * 60 * 60 * 16)) / 1000
		// 结束秒数
		let endM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.endTime ? data.endTime : props
			.endTime)) - (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
					.endTime)) - (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 * (start.value.offsetLeft + (start.value.clientWidth /
				2)))
		};
		document.onmouseup = (e) => {
			data.startLeft = start.value.clientWidth + start.value.offsetLeft
			// 开始秒数
			let startM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.startTime ? data.startTime : props
				.startTime)) - (1000 * 60 * 60 * 16)) / 1000
			playBySeconds(startM)
			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 * (end.value.offsetLeft + (end.value.clientWidth / 2)))
		};
		document.onmouseup = (e) => {
			data.endLeft = end.value.offsetLeft
			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;
	}

	.crop-filter {
		width: 100%;
		height: 0.5rem;
		padding: 0 0.4rem;
		box-sizing: border-box;
		display: flex;
		align-items: center;

		.timer-shaft {
			width: 100%;
			border-bottom: 0.04rem solid #56a0ee;
			border-radius: 0.1rem;
			position: relative;

			.circle {
				width: 0.12rem;
				height: 0.12rem;
				border-radius: 50%;
				position: absolute;
				top: -0.04rem;
				background-color: #fff;
				cursor: pointer;

				.timer {
					position: absolute;
					color: white;
					font-size: 0.1rem;
					left: -0.3rem;
					user-select: none;
				}
			}

			.strat-circle {
				left: -0.06rem;
				background-color: #ff14ec;

				.timer {
					bottom: -0.15rem;
				}
			}

			.end-circle {
				right: -0.06rem;

				.timer {
					top: -0.15rem;
				}
			}
		}
	}
</style>

.vue完整代码

<template>
	<video id="videoPlayer" @play="onplay" controls="true" preload="auto" muted class="video" width="100%"
		:src="props.url"></video>
	<div class="crop-filter">
		<div class="timer-shaft" ref="shaft">
			<div class="strat-circle circle" ref="start" @mousedown="startMouseDown">
				<div class="timer">
					{{ data.startTime||props.startTime}}
				</div>
			</div>
			<div class="end-circle circle" ref="end" @mousedown="endMouseDown">
				<div class="timer">
					{{data.endTime||props.endTime}}
				</div>
			</div>
		</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;
	}
	// 设置默认值,需要显式的开启,具体查看vue3文档
	const props = withDefaults(defineProps < Props > (), {
		startTime: '00:00:00.0',
		endTime: '00:00:08.0',
		url: '',
	})
	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
		getNowTime(time)
		data.endLeft = end.value.offsetLeft
		data.endright = end.value.offsetLeft
		data.startLeft = start.value.clientWidth - (start.value.clientWidth / 2)
		getVideoTime()
	})
	// 播放事件
	const onplay = () => {
		let myVideo: videoRef = document.getElementById('videoPlayer');
		// 开始秒数
		let startM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.startTime ? data.startTime : props
			.startTime)) - (1000 * 60 * 60 * 16)) / 1000
		// 结束秒数
		let endM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.endTime ? data.endTime : props
			.endTime)) - (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
					.endTime)) - (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 * (start.value.offsetLeft + (start.value.clientWidth /
				2)))
		};
		document.onmouseup = (e) => {
			data.startLeft = start.value.clientWidth + start.value.offsetLeft
			// 开始秒数
			let startM = (dateStrChangeTimeTamp('1970-01-02 ' + (data.startTime ? data.startTime : props
				.startTime)) - (1000 * 60 * 60 * 16)) / 1000
			playBySeconds(startM)
			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 * (end.value.offsetLeft + (end.value.clientWidth / 2)))
		};
		document.onmouseup = (e) => {
			data.endLeft = end.value.offsetLeft
			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;
	}

	.crop-filter {
		width: 100%;
		height: 0.5rem;
		padding: 0 0.4rem;
		box-sizing: border-box;
		display: flex;
		align-items: center;

		.timer-shaft {
			width: 100%;
			border-bottom: 0.04rem solid #56a0ee;
			border-radius: 0.1rem;
			position: relative;

			.circle {
				width: 0.12rem;
				height: 0.12rem;
				border-radius: 50%;
				position: absolute;
				top: -0.04rem;
				background-color: #fff;
				cursor: pointer;

				.timer {
					position: absolute;
					color: white;
					font-size: 0.1rem;
					left: -0.3rem;
					user-select: none;
				}
			}

			.strat-circle {
				left: -0.06rem;
				background-color: #ff14ec;

				.timer {
					bottom: -0.15rem;
				}
			}

			.end-circle {
				right: -0.06rem;

				.timer {
					top: -0.15rem;
				}
			}
		}
	}
</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;
}

//日期字符串转成时间戳
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
}

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值