欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一個Vue視頻媒體多段裁剪組件的實現(xiàn)示例

 更新時間:2018年08月09日 15:12:04   作者:fengma1992  
這篇文章主要介紹了一個Vue媒體多段裁剪組件的實現(xiàn)示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

近日項目有個新需求,需要對視頻或音頻進行多段裁剪然后拼接。例如,一段視頻長30分鐘,我需要將5-10分鐘、17-22分鐘、24-29分鐘這三段拼接到一起成一整段視頻。裁剪在前端,拼接在后端。

網(wǎng)上簡單找了找,基本都是客戶端內(nèi)的工具,沒有純網(wǎng)頁的裁剪。既然沒有,那就動手寫一個。

代碼已上傳到GitHub: https://github.com/fengma1992/media-cut-tool

廢話不多,下面就來看看怎么設計的。

效果圖

圖中底部的功能塊為裁剪工具組件,上方的視頻為演示用,當然也能是音頻。

功能特點:

  1. 支持鼠標拖拽輸入與鍵盤數(shù)字輸入兩種模式;
  2. 支持預覽播放指定裁剪片段;
  3. 左側鼠標輸入與右側鍵盤輸入聯(lián)動;
  4. 鼠標移動時自動捕捉高亮拖拽條;
  5. 確認裁剪時自動去重;

*注:項目中的圖標都替換成了文字

思路

整體來看,通過一個數(shù)據(jù)數(shù)組 cropItemList 來保存用戶輸入數(shù)據(jù),不管是鼠標拖拽還是鍵盤輸入,都來操作 cropItemList 實現(xiàn)兩側數(shù)據(jù)聯(lián)動。最后通過處理 cropItemList 來輸出用戶想要的裁剪。

cropItemList 結構如下:

cropItemList: [
 {
  startTime: 0, // 開始時間
  endTime: 100, // 結束時間
  startTimeArr: [hoursStr, minutesStr, secondsStr], // 時分秒字符串
  endTimeArr: [hoursStr, minutesStr, secondsStr], // 時分秒字符串
  startTimeIndicatorOffsetX: 0, // 開始時間在左側拖動區(qū)X偏移量
  endTimeIndicatorOffsetX: 100, // 結束時間在左側拖動區(qū)X偏移量
 }
]

第一步

既然是多段裁剪,那么用戶得知道裁剪了哪些時間段,這通過右側的裁剪列表來呈現(xiàn)。

列表

列表存在三個狀態(tài):

無數(shù)據(jù)狀態(tài)

無數(shù)據(jù)的時候顯示內(nèi)容為空,當用戶點擊輸入框時主動為他生成一條數(shù)據(jù),默認為視頻長度的1/4到3/4處。

有一條數(shù)據(jù)

此時界面顯示很簡單,將唯一一條數(shù)據(jù)呈現(xiàn)。

有多條數(shù)據(jù)

有多條數(shù)據(jù)時就得有額外處理了,因為第1條數(shù)據(jù)在最下方,而如果用 v-for 去循環(huán) cropItemList,那么就會出現(xiàn)下圖的狀況:

而且,第1條最右側是添加按鈕,而剩下的最右側都是刪除按鈕。所以,我們 將第1條單獨提出來寫,然后將 cropItemList 逆序生成一個 renderList 并循環(huán) renderList0 -> listLength - 2

即可。

<template v-for="(item, index) in renderList">
 <div v-if="index < listLength -1"
   :key="index"
   class="crop-time-item">
   ...
   ...
 </div>
</template>

下圖為最終效果:

時分秒輸入

這個其實就是寫三個 input 框,設 type="text" (設成 type=number 輸入框右側會有上下箭頭),然后通過監(jiān)聽input事件來保證輸入的正確性并更新數(shù)據(jù)。監(jiān)聽focus事件來確定是否需要在 cropItemList 為空時主動添加一條數(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>

播放片段

點擊播放按鈕時會通過 playingItem 記錄當前播放的片段,然后向上層發(fā)出 play 事件并帶上播放起始時間。同樣還有 pausestop 事件,來控制媒體暫停與停止。

<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ān)聽媒體的 timeupdate 事件并實時對比媒體的 currentTimeplayingItemendTime ,達到的時候就發(fā)出 pause 事件通知媒體暫停。

if (currentTime >= playingItem.endTime) {
 this.pause()
}

至此,鍵盤輸入的裁剪列表基本完成,下面介紹鼠標拖拽輸入。

第二步

下面介紹如何通過鼠標點擊與拖拽輸入。

1、確定鼠標交互邏輯

新增裁剪

鼠標在拖拽區(qū)點擊后,新增一條裁剪數(shù)據(jù),開始時間與結束時間均為 mouseup 時進度條的時間,并讓結束時間戳跟隨鼠標移動,進入編輯狀態(tài)。

確認時間戳

編輯狀態(tài),鼠標移動時,時間戳根據(jù)鼠標在進度條的當前位置來隨動,鼠標再次點擊后確認當前時間,并終止時間戳跟隨鼠標移動。

更改時間

非編輯狀態(tài),鼠標在進度條上移動時,監(jiān)聽 mousemove 事件,在接近任意一條裁剪數(shù)據(jù)的開始或結束時間戳時高亮當前數(shù)據(jù)并顯示時間戳。鼠標 mousedown 后選中時間戳并開始拖拽更改時間數(shù)據(jù)。 mouseup 后結束更改。

2、確定需要監(jiān)聽的鼠標事件

鼠標在進度條區(qū)域需要監(jiān)聽三個事件: mousedownmousemove 、 mouseup 。 在進度條區(qū)存在多種元素,簡單可分成三類:

  1. 鼠標移動時隨動的時間戳
  2. 存在裁剪片段時的開始時間戳、結束時間戳、淺藍色的時間遮罩
  3. 進度條本身

首先 mousedownmouseup 的監(jiān)聽當然是綁定在進度條本身。

this.timeLineContainer.addEventListener('mousedown', e => {
  const currentCursorOffsetX = e.clientX - containerLeft
  lastMouseDownOffsetX = currentCursorOffsetX
  // 檢測是否點到了時間戳
  this.timeIndicatorCheck(currentCursorOffsetX, 'mousedown')
 })
 
this.timeLineContainer.addEventListener('mouseup', e => {

 // 已經(jīng)處于裁剪狀態(tài)時,鼠標抬起,則裁剪狀態(tài)取消
 if (this.isCropping) {
  this.stopCropping()
  return
 }

 const currentCursorOffsetX = this.getFormattedOffsetX(e.clientX - containerLeft)
 // mousedown與mouseup位置不一致,則不認為是點擊,直接返回
 if (Math.abs(currentCursorOffsetX - lastMouseDownOffsetX) > 3) {
  return
 }

 // 更新當前鼠標指向的時間
 this.currentCursorTime = currentCursorOffsetX * this.timeToPixelRatio

 // 鼠標點擊新增裁剪片段
 if (!this.isCropping) {
  this.addNewCropItemInSlider()

  // 新操作位置為數(shù)組最后一位
  this.startCropping(this.cropItemList.length - 1)
 }
})

mousemove 這個,當非編輯狀態(tài)時,當然是監(jiān)聽進度條來實現(xiàn)時間戳隨動鼠標。而當需要選中開始或結束時間戳來進入編輯狀態(tài)時,我最初設想的是監(jiān)聽時間戳本身,來達到選中時間戳的目的。而實際情況是:當鼠標接近開始或結束時間戳時,一直有一個鼠標隨動的時間戳擋在前面,而且因為裁剪片段理論上可以無限增加,那我得監(jiān)聽2*裁剪片段個 mousemove 。

基于此,只在進度條本身監(jiān)聽 mousemove ,通過實時比對鼠標位置和時間戳位置來確定是否到了相應位置, 當然得加一個 throttle 節(jié)流。

this.timeLineContainer.addEventListener('mousemove', e => {
 throttle(() => {
  const currentCursorOffsetX = e.clientX - containerLeft
  // mousemove范圍檢測
  if (currentCursorOffsetX < 0 || currentCursorOffsetX > containerWidth) {
   this.isCursorIn = false
   // 鼠標拖拽狀態(tài)到達邊界直接觸發(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
  // 時間戳檢測
  this.timeIndicatorCheck(currentCursorOffsetX, 'mousemove')
  // 時間戳移動檢測
  this.timeIndicatorMove(currentCursorOffsetX)
 }, 10, true)()
})

3、實現(xiàn)拖拽與時間戳隨動

下面是時間戳檢測和時間戳移動檢測代碼

timeIndicatorCheck (currentCursorOffsetX, mouseEvent) {
 // 在裁剪狀態(tài),直接返回
 if (this.isCropping) {
  return
 }

 // 鼠標移動,重設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
  }

  // 默認始末時間戳在一起時優(yōu)先選中截止時間戳
  if (isCursorClose(item.endTimeIndicatorOffsetX, currentCursorOffsetX)) {
   this.endTimeIndicatorHoverIndex = index
   // 鼠標放下,開始裁剪
   if (mouseEvent === 'mousedown') {
    this.endTimeIndicatorDraggingIndex = index
    this.currentEditingIndex = index
    this.isCropping = true
   }
  }

  else if (isCursorClose(item.startTimeIndicatorOffsetX, currentCursorOffsetX)) {
   this.startTimeIndicatorHoverIndex = index
   // 鼠標放下,開始裁剪
   if (mouseEvent === 'mousedown') {
    this.startTimeIndicatorDraggingIndex = index
    this.currentEditingIndex = index
    this.isCropping = true
   }
  }
 })
},

timeIndicatorMove (currentCursorOffsetX) {
 // 裁剪狀態(tài),隨動時間戳
 if (this.isCropping) {
  const currentEditingIndex = this.currentEditingIndex
  const startTimeIndicatorDraggingIndex = this.startTimeIndicatorDraggingIndex
  const endTimeIndicatorDraggingIndex = this.endTimeIndicatorDraggingIndex
  const currentCursorTime = this.currentCursorTime

  let currentItem = this.cropItemList[currentEditingIndex]
  // 操作起始位時間戳
  if (startTimeIndicatorDraggingIndex > -1 && currentItem) {
   // 已到截止位時間戳則直接返回
   if (currentCursorOffsetX > currentItem.endTimeIndicatorOffsetX) {
    return
   }
   currentItem.startTimeIndicatorOffsetX = currentCursorOffsetX
   currentItem.startTime = currentCursorTime
  }

  // 操作截止位時間戳
  if (endTimeIndicatorDraggingIndex > -1 && currentItem) {
   // 已到起始位時間戳則直接返回
   if (currentCursorOffsetX < currentItem.startTimeIndicatorOffsetX) {
    return
   }
   currentItem.endTimeIndicatorOffsetX = currentCursorOffsetX
   currentItem.endTime = currentCursorTime
  }
  this.updateCropItem(currentItem, currentEditingIndex)
 }
}

第三步

裁剪完成后下一步當然是把數(shù)據(jù)丟給后端啦。

把用戶當:sweet_potato:(#紅薯#)

用戶使用的時候小手一抖,多點了一下 添加 按鈕,或者有帕金森,怎么都拖不準,就可能會有數(shù)據(jù)一樣或存在重合部分的裁剪片段。那么我們就得過濾掉重復和存在重合部分的裁剪。

還是直接看代碼方便

/**
 * 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

 // 遍歷,刪除重復片段
 cropItemList.forEach((item, index) => {
  // 遍歷到最后一項,直接寫入
  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無重疊,向列表添加一項,更新記錄參數(shù)
  if (item.startTime > endTime) {
   tempCropItemList.push({
    startTime: startTime,
    endTime: endTime,
    startTimeArr: formatTime.getFormatTimeArr(startTime),
    endTimeArr: formatTime.getFormatTimeArr(endTime),
   })
   // 標志量移到當前item
   startTime = item.startTime
   endTime = item.endTime
  }
 })

 return tempCropItemList
}

第四步

使用裁剪工具: 通過props及emit事件實現(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é) 列表增刪時的高度動畫

UI提了個需求,最多展示10條裁剪片段,超過了之后就滾動,還得有增刪動畫。本來以為直接設個 max-height 完事,結果發(fā)現(xiàn)

CSS的 transition 動畫只有針對絕對值的height有效 ,這就有點小麻煩,因為裁剪條數(shù)是變化的,那么高度也是在變化的。設絕對值該怎么辦呢。。。

這里通過HTML中tag的 attribute 屬性 data-count 來告訴CSS我現(xiàn)在有幾條裁剪,然后讓CSS根據(jù) data-count 來設置列表高度。

<!--超過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 時事件的 currentTarget 問題

因為存在DOM事件的捕獲與冒泡,而進度條上面可能有別的如時間戳、裁剪片段等元素, mousemove 事件的 currentTarget 可能會變,導致取鼠標距離進度條最左側的 offsetX 可能有問題;而如果通過檢測 currentTarget 是否為進度條也存在問題,因為鼠標移動的時候一直有個時間戳在隨動,導致偶爾一段時間都觸發(fā)不了進度條對應的 mousemove 事件。

解決辦法就是,頁面加載完成后取得進度條最左側距頁面最左側的距離, mousemove 事件不取 offsetX ,轉而取基于頁面最左側的 clientX ,然后兩者相減就得到了鼠標距離進度條最左側的像素值。代碼在上文中的添加 mousemove 監(jiān)聽里已寫。

時間格式化

因為裁剪工具很多地方需要將秒轉換為 00:00:00 格式的字符串,因此寫了一個工具函數(shù):輸入秒,輸出一個包含 dd,HH,mm,ss 四個 keyObject ,每個 key 為長度為2的字符串。用ES8的 String.prototype.padStart() 方法實現(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')
 };

}

以上就是本文的全部內(nèi)容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。

相關文章

  • 一文輕松了解v-model及其修飾符

    一文輕松了解v-model及其修飾符

    這篇文章主要給大家介紹了關于v-model及其修飾符的相關資料,v-model指令有三個可以選用的修飾符:.lazy、.number以及.trim,本文通過示例代碼介紹的非常詳細,需要的朋友可以參考下
    2021-11-11
  • vue中使用echarts制作圓環(huán)圖的實例代碼

    vue中使用echarts制作圓環(huán)圖的實例代碼

    這篇文章主要介紹了vue中使用echarts制作圓環(huán)圖的實例代碼,代碼簡單易懂,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下
    2018-07-07
  • Vue數(shù)據(jù)雙向綁定原理實例解析

    Vue數(shù)據(jù)雙向綁定原理實例解析

    這篇文章主要介紹了Vue數(shù)據(jù)雙向綁定原理實例解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下
    2020-05-05
  • Vue3菜單展開和收起實現(xiàn)

    Vue3菜單展開和收起實現(xiàn)

    在Vue項目中實現(xiàn)首頁布局,包括可收放的左側菜單和主體內(nèi)容區(qū),在store中管理菜單狀態(tài),通過修改isCollapse狀態(tài)控制菜單的展開與收起,在home.vue中編寫左側菜單欄的代碼和樣式,實現(xiàn)一個響應式的用戶界面
    2024-09-09
  • Tree-Shaking?機制快速掌握

    Tree-Shaking?機制快速掌握

    這篇文章主要為大家介紹了Tree-Shaking?機制的快速掌握教程,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-02-02
  • Vue2.0使用過程常見的一些問題總結學習

    Vue2.0使用過程常見的一些問題總結學習

    本篇文章主要介紹了Vue2.0使用過程常見的一些問題總結學習,詳細的介紹了使用中會遇到的各種錯誤,有興趣的可以了解一下。
    2017-04-04
  • 在Vant的基礎上封裝下拉日期控件的代碼示例

    在Vant的基礎上封裝下拉日期控件的代碼示例

    這篇文章主要介紹了在Vant的基礎上封裝下拉日期控件的代碼示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-12-12
  • vue3實現(xiàn)局部頁面刷新效果的示例詳解

    vue3實現(xiàn)局部頁面刷新效果的示例詳解

    這篇文章主要為大家詳細介紹了vue3如何采用 App.vue定義全局變量與方法并實現(xiàn)局部頁面刷新效果,文中的示例代碼講解詳細,需要的可以參考一下
    2024-01-01
  • Vue使用$set和$delete操作對象屬性

    Vue使用$set和$delete操作對象屬性

    這篇文章介紹了Vue使用$set和$delete操作對象屬性的方法,文中通過示例代碼介紹的非常詳細。對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2022-03-03
  • 詳解vuex狀態(tài)管理模式

    詳解vuex狀態(tài)管理模式

    這篇文章主要介紹了詳解vuex狀態(tài)管理模式,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-11-11

最新評論