Skip to content

运维无忧

  • 首页
  • 技术资料
    • linux
    • nginx
    • wordpress
    • 前端开发
    • vps
  • 视频教学
    • 编程
    • 主机
  • 用户中心
  • Documentation
  • 首页
  • 技术资料
    • linux
    • nginx
    • wordpress
    • 前端开发
    • vps
  • 视频教学
    • 编程
    • 主机
  • 用户中心
  • Documentation
  1. Home
  2. / 技术资料
  3. / 前端开发
  4. / vue3+ts实现视频根据时间轴截取,并可以通过传入截取起止时间进行当前剪辑的回显

vue3+ts实现视频根据时间轴截取,并可以通过传入截取起止时间进行当前剪辑的回显

admin前端开发技术资料2022年11月1日(0)

转载  https://blog.csdn.net/wed2019/article/details/126995825?spm=1001.2014.3001.5502

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

穿插一个简化版本,时间轴是一条线,功能比这个简化,或许会符合部分人的需求。

链接http://t.csdn.cn/F3UxJ

功能概述
通过传入源视频时长,源视频的视频地址,当前剪辑的开始时间,当前剪辑的结束时间和关键帧缩略图(需要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

vue

文章导航

Previousnginx使用error_page实现跳转到https
Next koa各种文件上传攻略,从小图片到大文件断点续传

标签

https nginx vue wordpress

© Copyright 2025 运维无忧 All Rights Reserved.

Proudly powered by WordPress | Theme: Fox009 Wisdom by Fox009.