您好,登錄后才能下訂單哦!
這期內(nèi)容當(dāng)中小編將會給大家?guī)碛嘘P(guān)使用Vue怎么實(shí)現(xiàn)視頻媒體多段裁剪組件,文章內(nèi)容豐富且以專業(yè)的角度為大家分析和敘述,閱讀完這篇文章希望大家可以有所收獲。
功能特點(diǎn):
支持鼠標(biāo)拖拽輸入與鍵盤數(shù)字輸入兩種模式;
支持預(yù)覽播放指定裁剪片段;
左側(cè)鼠標(biāo)輸入與右側(cè)鍵盤輸入聯(lián)動;
鼠標(biāo)移動時(shí)自動捕捉高亮拖拽條;
確認(rèn)裁剪時(shí)自動去重;
*注:項(xiàng)目中的圖標(biāo)都替換成了文字
思路
整體來看,通過一個(gè)數(shù)據(jù)數(shù)組 cropItemList
來保存用戶輸入數(shù)據(jù),不管是鼠標(biāo)拖拽還是鍵盤輸入,都來操作 cropItemList
實(shí)現(xiàn)兩側(cè)數(shù)據(jù)聯(lián)動。最后通過處理 cropItemList
來輸出用戶想要的裁剪。
cropItemList
結(jié)構(gòu)如下:
cropItemList: [ { startTime: 0, // 開始時(shí)間 endTime: 100, // 結(jié)束時(shí)間 startTimeArr: [hoursStr, minutesStr, secondsStr], // 時(shí)分秒字符串 endTimeArr: [hoursStr, minutesStr, secondsStr], // 時(shí)分秒字符串 startTimeIndicatorOffsetX: 0, // 開始時(shí)間在左側(cè)拖動區(qū)X偏移量 endTimeIndicatorOffsetX: 100, // 結(jié)束時(shí)間在左側(cè)拖動區(qū)X偏移量 } ]
第一步
既然是多段裁剪,那么用戶得知道裁剪了哪些時(shí)間段,這通過右側(cè)的裁剪列表來呈現(xiàn)。
列表
列表存在三個(gè)狀態(tài):
無數(shù)據(jù)狀態(tài)
無數(shù)據(jù)的時(shí)候顯示內(nèi)容為空,當(dāng)用戶點(diǎn)擊輸入框時(shí)主動為他生成一條數(shù)據(jù),默認(rèn)為視頻長度的1/4到3/4處。
有一條數(shù)據(jù)
此時(shí)界面顯示很簡單,將唯一一條數(shù)據(jù)呈現(xiàn)。
有多條數(shù)據(jù)
有多條數(shù)據(jù)時(shí)就得有額外處理了,因?yàn)榈?條數(shù)據(jù)在最下方,而如果用 v-for
去循環(huán) cropItemList
,那么就會出現(xiàn)下圖的狀況:
而且,第1條最右側(cè)是添加按鈕,而剩下的最右側(cè)都是刪除按鈕。所以,我們 將第1條單獨(dú)提出來寫,然后將 cropItemList
逆序生成一個(gè) renderList
并循環(huán) renderList
的 0 -> listLength - 2
條
即可。
<template v-for="(item, index) in renderList"> <div v-if="index < listLength -1" :key="index" class="crop-time-item"> ... ... </div> </template>
下圖為最終效果:
時(shí)分秒輸入
這個(gè)其實(shí)就是寫三個(gè) input
框,設(shè) type="text"
(設(shè)成 type=number
輸入框右側(cè)會有上下箭頭),然后通過監(jiān)聽input事件來保證輸入的正確性并更新數(shù)據(jù)。監(jiān)聽focus事件來確定是否需要在 cropItemList
為空時(shí)主動添加一條數(shù)據(jù)。
<div class="time-input"> <input type="text" :value="renderList[listLength -1] && renderList[listLength -1].startTimeArr[0]" @input="startTimeChange($event, 0, 0)" @focus="inputFocus()"/> : <input type="text" :value="renderList[listLength -1] && renderList[listLength -1].startTimeArr[1]" @input="startTimeChange($event, 0, 1)" @focus="inputFocus()"/> : <input type="text" :value="renderList[listLength -1] && renderList[listLength -1].startTimeArr[2]" @input="startTimeChange($event, 0, 2)" @focus="inputFocus()"/> </div>
播放片段
點(diǎn)擊播放按鈕時(shí)會通過 playingItem
記錄當(dāng)前播放的片段,然后向上層發(fā)出 play
事件并帶上播放起始時(shí)間。同樣還有 pause
和 stop
事件,來控制媒體暫停與停止。
<CropTool :duration="duration" :playing="playing" :currentPlayingTime="currentTime" @play="playVideo" @pause="pauseVideo" @stop="stopVideo"/>
/** * 播放選中片段 * @param index */ playSelectedClip: function (index) { if (!this.listLength) { console.log('無裁剪片段') return } this.playingItem = this.cropItemList[index] this.playingIndex = index this.isCropping = false this.$emit('play', this.playingItem.startTime || 0) }
這里控制了開始播放,那么如何讓媒體播到裁剪結(jié)束時(shí)間的時(shí)候自動停止呢?
監(jiān)聽媒體的 timeupdate
事件并實(shí)時(shí)對比媒體的 currentTime
與 playingItem
的 endTime
,達(dá)到的時(shí)候就發(fā)出 pause
事件通知媒體暫停。
if (currentTime >= playingItem.endTime) { this.pause() }
至此,鍵盤輸入的裁剪列表基本完成,下面介紹鼠標(biāo)拖拽輸入。
第二步
下面介紹如何通過鼠標(biāo)點(diǎn)擊與拖拽輸入。
1、確定鼠標(biāo)交互邏輯
新增裁剪
鼠標(biāo)在拖拽區(qū)點(diǎn)擊后,新增一條裁剪數(shù)據(jù),開始時(shí)間與結(jié)束時(shí)間均為 mouseup
時(shí)進(jìn)度條的時(shí)間,并讓結(jié)束時(shí)間戳跟隨鼠標(biāo)移動,進(jìn)入編輯狀態(tài)。
確認(rèn)時(shí)間戳
編輯狀態(tài),鼠標(biāo)移動時(shí),時(shí)間戳根據(jù)鼠標(biāo)在進(jìn)度條的當(dāng)前位置來隨動,鼠標(biāo)再次點(diǎn)擊后確認(rèn)當(dāng)前時(shí)間,并終止時(shí)間戳跟隨鼠標(biāo)移動。
更改時(shí)間
非編輯狀態(tài),鼠標(biāo)在進(jìn)度條上移動時(shí),監(jiān)聽 mousemove
事件,在接近任意一條裁剪數(shù)據(jù)的開始或結(jié)束時(shí)間戳?xí)r高亮當(dāng)前數(shù)據(jù)并顯示時(shí)間戳。鼠標(biāo) mousedown
后選中時(shí)間戳并開始拖拽更改時(shí)間數(shù)據(jù)。 mouseup
后結(jié)束更改。
2、確定需要監(jiān)聽的鼠標(biāo)事件
鼠標(biāo)在進(jìn)度條區(qū)域需要監(jiān)聽三個(gè)事件: mousedown
、 mousemove
、 mouseup
。 在進(jìn)度條區(qū)存在多種元素,簡單可分成三類:
鼠標(biāo)移動時(shí)隨動的時(shí)間戳
存在裁剪片段時(shí)的開始時(shí)間戳、結(jié)束時(shí)間戳、淺藍(lán)色的時(shí)間遮罩
進(jìn)度條本身
首先 mousedown
和 mouseup
的監(jiān)聽當(dāng)然是綁定在進(jìn)度條本身。
this.timeLineContainer.addEventListener('mousedown', e => { const currentCursorOffsetX = e.clientX - containerLeft lastMouseDownOffsetX = currentCursorOffsetX // 檢測是否點(diǎn)到了時(shí)間戳 this.timeIndicatorCheck(currentCursorOffsetX, 'mousedown') }) this.timeLineContainer.addEventListener('mouseup', e => { // 已經(jīng)處于裁剪狀態(tài)時(shí),鼠標(biāo)抬起,則裁剪狀態(tài)取消 if (this.isCropping) { this.stopCropping() return } const currentCursorOffsetX = this.getFormattedOffsetX(e.clientX - containerLeft) // mousedown與mouseup位置不一致,則不認(rèn)為是點(diǎn)擊,直接返回 if (Math.abs(currentCursorOffsetX - lastMouseDownOffsetX) > 3) { return } // 更新當(dāng)前鼠標(biāo)指向的時(shí)間 this.currentCursorTime = currentCursorOffsetX * this.timeToPixelRatio // 鼠標(biāo)點(diǎn)擊新增裁剪片段 if (!this.isCropping) { this.addNewCropItemInSlider() // 新操作位置為數(shù)組最后一位 this.startCropping(this.cropItemList.length - 1) } })
mousemove
這個(gè),當(dāng)非編輯狀態(tài)時(shí),當(dāng)然是監(jiān)聽進(jìn)度條來實(shí)現(xiàn)時(shí)間戳隨動鼠標(biāo)。而當(dāng)需要選中開始或結(jié)束時(shí)間戳來進(jìn)入編輯狀態(tài)時(shí),我最初設(shè)想的是監(jiān)聽時(shí)間戳本身,來達(dá)到選中時(shí)間戳的目的。而實(shí)際情況是:當(dāng)鼠標(biāo)接近開始或結(jié)束時(shí)間戳?xí)r,一直有一個(gè)鼠標(biāo)隨動的時(shí)間戳擋在前面,而且因?yàn)椴眉羝卫碚撋峡梢詿o限增加,那我得監(jiān)聽2*裁剪片段個(gè) mousemove
。
基于此,只在進(jìn)度條本身監(jiān)聽 mousemove
,通過實(shí)時(shí)比對鼠標(biāo)位置和時(shí)間戳位置來確定是否到了相應(yīng)位置, 當(dāng)然得加一個(gè) throttle
節(jié)流。
this.timeLineContainer.addEventListener('mousemove', e => { throttle(() => { const currentCursorOffsetX = e.clientX - containerLeft // mousemove范圍檢測 if (currentCursorOffsetX < 0 || currentCursorOffsetX > containerWidth) { this.isCursorIn = false // 鼠標(biāo)拖拽狀態(tài)到達(dá)邊界直接觸發(fā)mouseup狀態(tài) if (this.isCropping) { this.stopCropping() this.timeIndicatorCheck(currentCursorOffsetX < 0 ? 0 : containerWidth, 'mouseup') } return } else { this.isCursorIn = true } this.currentCursorTime = currentCursorOffsetX * this.timeToPixelRatio this.currentCursorOffsetX = currentCursorOffsetX // 時(shí)間戳檢測 this.timeIndicatorCheck(currentCursorOffsetX, 'mousemove') // 時(shí)間戳移動檢測 this.timeIndicatorMove(currentCursorOffsetX) }, 10, true)() })
3、實(shí)現(xiàn)拖拽與時(shí)間戳隨動
下面是時(shí)間戳檢測和時(shí)間戳移動檢測代碼
timeIndicatorCheck (currentCursorOffsetX, mouseEvent) { // 在裁剪狀態(tài),直接返回 if (this.isCropping) { return } // 鼠標(biāo)移動,重設(shè)hover狀態(tài) this.startTimeIndicatorHoverIndex = -1 this.endTimeIndicatorHoverIndex = -1 this.startTimeIndicatorDraggingIndex = -1 this.endTimeIndicatorDraggingIndex = -1 this.cropItemHoverIndex = -1 this.cropItemList.forEach((item, index) => { if (currentCursorOffsetX >= item.startTimeIndicatorOffsetX && currentCursorOffsetX <= item.endTimeIndicatorOffsetX) { this.cropItemHoverIndex = index } // 默認(rèn)始末時(shí)間戳在一起時(shí)優(yōu)先選中截止時(shí)間戳 if (isCursorClose(item.endTimeIndicatorOffsetX, currentCursorOffsetX)) { this.endTimeIndicatorHoverIndex = index // 鼠標(biāo)放下,開始裁剪 if (mouseEvent === 'mousedown') { this.endTimeIndicatorDraggingIndex = index this.currentEditingIndex = index this.isCropping = true } } else if (isCursorClose(item.startTimeIndicatorOffsetX, currentCursorOffsetX)) { this.startTimeIndicatorHoverIndex = index // 鼠標(biāo)放下,開始裁剪 if (mouseEvent === 'mousedown') { this.startTimeIndicatorDraggingIndex = index this.currentEditingIndex = index this.isCropping = true } } }) }, timeIndicatorMove (currentCursorOffsetX) { // 裁剪狀態(tài),隨動時(shí)間戳 if (this.isCropping) { const currentEditingIndex = this.currentEditingIndex const startTimeIndicatorDraggingIndex = this.startTimeIndicatorDraggingIndex const endTimeIndicatorDraggingIndex = this.endTimeIndicatorDraggingIndex const currentCursorTime = this.currentCursorTime let currentItem = this.cropItemList[currentEditingIndex] // 操作起始位時(shí)間戳 if (startTimeIndicatorDraggingIndex > -1 && currentItem) { // 已到截止位時(shí)間戳則直接返回 if (currentCursorOffsetX > currentItem.endTimeIndicatorOffsetX) { return } currentItem.startTimeIndicatorOffsetX = currentCursorOffsetX currentItem.startTime = currentCursorTime } // 操作截止位時(shí)間戳 if (endTimeIndicatorDraggingIndex > -1 && currentItem) { // 已到起始位時(shí)間戳則直接返回 if (currentCursorOffsetX < currentItem.startTimeIndicatorOffsetX) { return } currentItem.endTimeIndicatorOffsetX = currentCursorOffsetX currentItem.endTime = currentCursorTime } this.updateCropItem(currentItem, currentEditingIndex) } }
第三步
裁剪完成后下一步當(dāng)然是把數(shù)據(jù)丟給后端啦。
把用戶當(dāng):sweet_potato:(#紅薯#)
用戶使用的時(shí)候小手一抖,多點(diǎn)了一下 添加
按鈕,或者有帕金森,怎么都拖不準(zhǔn),就可能會有數(shù)據(jù)一樣或存在重合部分的裁剪片段。那么我們就得過濾掉重復(fù)和存在重合部分的裁剪。
還是直接看代碼方便
/** * cropItemList排序并去重 */ cleanCropItemList () { let cropItemList = this.cropItemList // 1. 依據(jù)startTime由小到大排序 cropItemList = cropItemList.sort(function (item1, item2) { return item1.startTime - item2.startTime }) let tempCropItemList = [] let startTime = cropItemList[0].startTime let endTime = cropItemList[0].endTime const lastIndex = cropItemList.length - 1 // 遍歷,刪除重復(fù)片段 cropItemList.forEach((item, index) => { // 遍歷到最后一項(xiàng),直接寫入 if (lastIndex === index) { tempCropItemList.push({ startTime: startTime, endTime: endTime, startTimeArr: formatTime.getFormatTimeArr(startTime), endTimeArr: formatTime.getFormatTimeArr(endTime), }) return } // currentItem片段包含item if (item.endTime <= endTime && item.startTime >= startTime) { return } // currentItem片段與item有重疊 if (item.startTime <= endTime && item.endTime >= endTime) { endTime = item.endTime return } // currentItem片段與item無重疊,向列表添加一項(xiàng),更新記錄參數(shù) if (item.startTime > endTime) { tempCropItemList.push({ startTime: startTime, endTime: endTime, startTimeArr: formatTime.getFormatTimeArr(startTime), endTimeArr: formatTime.getFormatTimeArr(endTime), }) // 標(biāo)志量移到當(dāng)前item startTime = item.startTime endTime = item.endTime } }) return tempCropItemList }
第四步
使用裁剪工具: 通過props及emit事件實(shí)現(xiàn)媒體與裁剪工具之間的通信。
<template> <div id="app"> <video ref="video" src="https://pan.prprpr.me/?/dplayer/hikarunara.mp4" controls width="600px"> </video> <CropTool :duration="duration" :playing="playing" :currentPlayingTime="currentTime" @play="playVideo" @pause="pauseVideo" @stop="stopVideo"/> </div> </template> <script> import CropTool from './components/CropTool.vue' export default { name: 'app', components: { CropTool, }, data () { return { duration: 0, playing: false, currentTime: 0, } }, mounted () { const videoElement = this.$refs.video videoElement.ondurationchange = () => { this.duration = videoElement.duration } videoElement.onplaying = () => { this.playing = true } videoElement.onpause = () => { this.playing = false } videoElement.ontimeupdate = () => { this.currentTime = videoElement.currentTime } }, methods: { seekVideo (seekTime) { this.$refs.video.currentTime = seekTime }, playVideo (time) { this.seekVideo(time) this.$refs.video.play() }, pauseVideo () { this.$refs.video.pause() }, stopVideo () { this.$refs.video.pause() this.$refs.video.currentTime = 0 }, }, } </script>
總結(jié)
寫博客比寫代碼難多了,感覺很混亂的寫完了這個(gè)博客。
幾個(gè)小細(xì)節(jié) 列表增刪時(shí)的高度動畫
UI提了個(gè)需求,最多展示10條裁剪片段,超過了之后就滾動,還得有增刪動畫。本來以為直接設(shè)個(gè) max-height
完事,結(jié)果發(fā)現(xiàn)
CSS的 transition
動畫只有針對絕對值的height有效 ,這就有點(diǎn)小麻煩,因?yàn)椴眉魲l數(shù)是變化的,那么高度也是在變化的。設(shè)絕對值該怎么辦呢。。。
這里通過HTML中tag的 attribute
屬性 data-count
來告訴CSS我現(xiàn)在有幾條裁剪,然后讓CSS根據(jù) data-count
來設(shè)置列表高度。
<!--超過10條數(shù)據(jù)也只傳10,讓列表滾動--> <div class="crop-time-body" :data-count="listLength > 10 ? 10 : listLength -1"> </div>
.crop-time-body { overflow-y: auto; overflow-x: hidden; transition: height .5s; &[data-count="0"] { height: 0; } &[data-count="1"] { height: 40px; } &[data-count="2"] { height: 80px; } ... ... &[data-count="10"] { height: 380px; } }
mousemove
時(shí)事件的 currentTarget
問題
因?yàn)榇嬖贒OM事件的捕獲與冒泡,而進(jìn)度條上面可能有別的如時(shí)間戳、裁剪片段等元素, mousemove
事件的 currentTarget
可能會變,導(dǎo)致取鼠標(biāo)距離進(jìn)度條最左側(cè)的 offsetX
可能有問題;而如果通過檢測 currentTarget
是否為進(jìn)度條也存在問題,因?yàn)槭髽?biāo)移動的時(shí)候一直有個(gè)時(shí)間戳在隨動,導(dǎo)致偶爾一段時(shí)間都觸發(fā)不了進(jìn)度條對應(yīng)的 mousemove
事件。
解決辦法就是,頁面加載完成后取得進(jìn)度條最左側(cè)距頁面最左側(cè)的距離, mousemove
事件不取 offsetX
,轉(zhuǎn)而取基于頁面最左側(cè)的 clientX
,然后兩者相減就得到了鼠標(biāo)距離進(jìn)度條最左側(cè)的像素值。代碼在上文中的添加 mousemove
監(jiān)聽里已寫。
時(shí)間格式化
因?yàn)椴眉艄ぞ吆芏嗟胤叫枰獙⒚朕D(zhuǎn)換為 00:00:00
格式的字符串,因此寫了一個(gè)工具函數(shù):輸入秒,輸出一個(gè)包含 dd,HH,mm,ss
四個(gè) key
的 Object
,每個(gè) key
為長度為2的字符串。用ES8的 String.prototype.padStart() 方法實(shí)現(xiàn)。
export default function (seconds) { const date = new Date(seconds * 1000); return { days: String(date.getUTCDate() - 1).padStart(2, '0'), hours: String(date.getUTCHours()).padStart(2, '0'), minutes: String(date.getUTCMinutes()).padStart(2, '0'), seconds: String(date.getUTCSeconds()).padStart(2, '0') }; }
上述就是小編為大家分享的使用Vue怎么實(shí)現(xiàn)視頻媒體多段裁剪組件了,如果剛好有類似的疑惑,不妨參照上述分析進(jìn)行理解。如果想知道更多相關(guān)知識,歡迎關(guān)注億速云行業(yè)資訊頻道。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。