changelog
2020.04.20 Исправлен ряд ошибок, оптимизированы некоторые анимации, оптимизирован режим взаимодействия.
предисловие
В последнее время в проекте появилось новое требование, требующее многократной обрезки и склейки видео или аудио. Например, видео длится 30 минут, и мне нужно соединить три сегмента 5-10 минут, 17-22 минуты и 24-29 минут вместе в единое видео. Укороченный спереди, простроченный сзади.
Я просто искал в Интернете, и это в основном инструменты в клиенте, без вырезания чистых веб-страниц. Если у вас его нет, напишите его сами.
Код загружен на GitHub
Добро пожаловать в ЗвездуGitHub.com/Сумасшедшая лошадь1992/…
Не много ерунды, давайте посмотрим, как она устроена.
визуализация
Функциональный блок в нижней части рисунка — компонент инструмента обрезки, а видео выше — для демонстрации, и, конечно, это может быть и звук.
Функции:
- Поддерживает два режима ввода с помощью перетаскивания мышью и ввода с клавиатуры;
- Поддержка предварительного просмотра указанных клипов;
- Ввод левой кнопкой мыши связан с вводом правой клавиатуры;
- Автоматически захватывать выделенную полосу перетаскивания при перемещении мыши;
- Автоматическая дедупликация при подтверждении кадрирования;
*Примечание: значки в проекте заменены текстом
идеи
Всего через массив данныхcropItemList
Чтобы сохранить данные пользователя ввода, будь то перетаскивание мыши или ввод клавиатуры, работаютcropItemList
Реализуйте связь данных с обеих сторон. Наконец, обработавcropItemList
для вывода урожая, который хочет пользователь.
cropItemList
Структура выглядит следующим образом:
cropItemList: [
{
startTime: 0, // 开始时间
endTime: 100, // 结束时间
startTimeArr: [hoursStr, minutesStr, secondsStr], // 时分秒字符串
endTimeArr: [hoursStr, minutesStr, secondsStr], // 时分秒字符串
startTimeIndicatorOffsetX: 0, // 开始时间在左侧拖动区X偏移量
endTimeIndicatorOffsetX: 100, // 结束时间在左侧拖动区X偏移量
}
]
первый шаг
Поскольку это многосегментная культура, пользователь должен знать, какие периоды времени обрезаются, что представлено в списке культур справа.
список
Списки существуют в трех состояниях:
- нет статуса данных
- есть данные
- несколько данных
v-for
зацикливатьсяcropItemList
, то получится следующая ситуация:Кроме того, самая правая часть первой панели — это кнопка добавления, а остальные крайние правые — кнопки удаления. поэтому мыНапишите пункт 1 отдельно, затем напишитеcropItemList
генерировать обратный порядокrenderList
и петля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>
На фото ниже конечный результат:
Введите часы, минуты и секунды
Это на самом деле триinput
рамка, наборtype="text"
(установлен вtype=number
В правой части поля ввода будут стрелки вверх и вниз), а затем прослушайте событие ввода, чтобы убедиться в правильности ввода и обновить данные. Прослушайте событие фокуса, чтобы определить, нужно лиcropItemList
Пустая инициатива по добавлению данных.
<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
Запишите воспроизводимый в данный момент клип и отправьте его на верхний уровеньplay
событие со временем начала воспроизведения. Также есть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)
}
Здесь контролируется начало воспроизведения, так как же сделать так, чтобы мультимедиа автоматически останавливалось, когда оно достигает конечного времени обрезки?
следить за СМИtimeupdate
события и сравнивать медиа в режиме реального времениcurrentTime
а такжеplayingItem
изendTime
, выдается при достиженииpause
Приостановка мультимедиа уведомления о событии.
if (currentTime >= playingItem.endTime) {
this.pause()
}
На этом список обрезки ввода с клавиатуры в основном завершен, и ниже описывается ввод с помощью перетаскивания мышью.
второй шаг
Ниже описано, как вводить с помощью щелчка мыши и перетаскивания.
1. Определить логику взаимодействия с мышью
-
Добавить обрезку
После щелчка мышью в области перетаскивания добавляются новые данные отсечения, а также время начала и время окончания.
mouseup
Засеките время индикатора выполнения и позвольте отметке времени окончания переместиться с помощью мыши, чтобы войти в состояние редактирования. -
временная метка подтверждения
В состоянии редактирования, когда мышь перемещается, отметка времени следует за текущим положением мыши на индикаторе выполнения.После повторного щелчка мыши текущее время подтверждается, и отметка времени прерывается, чтобы следовать за движением мыши.
-
Время смены
В нередактируемом состоянии, когда мышь перемещается по индикатору выполнения, отслеживайте
mousemove
Событие, которое выделяет текущие данные и отображает временную метку, когда она приближается к начальной или конечной временной метке любого фрагмента обрезанных данных. мышьmousedown
Затем выберите метку времени и начните перетаскивать, чтобы изменить данные времени.mouseup
чтобы закончить изменения.
2. Определите события мыши, которые необходимо отслеживать
Мышь должна отслеживать три события в области индикатора выполнения:mousedown
,mousemove
,mouseup
.
В области индикатора выполнения есть различные элементы, которые можно легко разделить на три категории:
- Отметка времени, которая следует за движением мыши
- Отметка времени начала, отметка времени окончания, голубая маска времени при наличии вырезающих клипов
- Сам прогресс
первыйmousedown
а такжеmouseup
Слушатель, конечно, привязан к самому индикатору выполнения.
this.timeLineContainer.addEventListener('mousedown', e => {
const currentCursorOffsetX = e.clientX - containerLeft
lastMouseDownOffsetX = currentCursorOffsetX
// 检测是否点到了时间戳
this.timeIndicatorCheck(currentCursorOffsetX, 'mousedown')
})
this.timeLineContainer.addEventListener('mouseup', e => {
// 已经处于裁剪状态时,鼠标抬起,则裁剪状态取消
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()
// 新操作位置为数组最后一位
this.startCropping(this.cropItemList.length - 1)
}
})
mousemove
Это, когда состояние без редактирования, конечно, заключается в наблюдении за индикатором выполнения, чтобы реализовать временную метку, чтобы следовать за мышью. Когда мне нужно выбрать начальную или конечную временную метку, чтобы войти в состояние редактирования, моя первоначальная идея состоит в том, чтобы отслеживать саму временную метку, чтобы достичь цели выбора временной метки. Фактическая ситуация такова: когда мышь находится близко к начальной или конечной временной метке, перед ней всегда есть временная метка, за которой следует мышь, и поскольку отсеченные клипы теоретически могут увеличиваться бесконечно, я должен отслеживать 2 * отсеченные клипы.mousemove
.
Исходя из этого, слушайте только сам индикатор выполненияmousemove
, сравнивая положение мыши и положение метки времени в реальном времени, чтобы определить, была ли достигнута соответствующая позиция, конечно, нужно добавитьthrottle
дросселирование.
this.timeLineContainer.addEventListener('mousemove', e => {
throttle(() => {
const currentCursorOffsetX = e.clientX - containerLeft
// mousemove范围检测
if (currentCursorOffsetX < 0 || currentCursorOffsetX > containerWidth) {
this.isCursorIn = false
// 鼠标拖拽状态到达边界直接触发mouseup状态
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. Реализуйте перетаскивание и отметку времени.
Первый — это захват метки времени, когдаmousemove
, пройтись по всем вырезаемым клипам, чтобы определить, близко ли текущее положение мыши к временной метке вырезаемого клипа.Если разница между положением мыши и временной меткой меньше 2, считается, что она близка (2 пикселя). спектр).
/**
* 检测鼠标是否接近
* @param x1
* @param x2
*/
const isCursorClose = function (x1, x2) {
return Math.abs(x1 - x2) < 2
}
обнаружено какtrue
Затем выделите метку времени и сегмент, соответствующий метке времени, и передайтеcropItemHoverIndex
переменная для записи текущей мышиhover
метка времени,
В то же время мышьmousedown
необязательныйhover
отметка времени и перетаскивание.
Ниже приведен код обнаружения временной метки и обнаружения перетаскивания временной метки.
timeIndicatorCheck (currentCursorOffsetX, mouseEvent) {
// 在裁剪状态,直接返回
if (this.isCropping) {
return
}
// 鼠标移动,重设hover状态
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
}
// 默认始末时间戳在一起时优先选中截止时间戳
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) {
// 裁剪状态,随动时间戳
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)
}
}
третий шаг
После того, как обрезка завершена, следующим шагом, конечно же, является передача данных в серверную часть.
Относитесь к пользователям как к 🍠 (#сладкий картофель#)
Когда пользователь использует его, маленькая рука дрожит и чаще щелкает添加
Если кнопка или кнопка Паркинсона перетаскиваются неправильно, могут быть обрезанные клипы с одинаковыми данными или перекрывающимися частями. Затем нам нужно отфильтровать дубликаты и объединить перекрывающиеся клипы в один сегмент.
Или просто посмотрите на код напрямую
/**
* cropItemList排序并去重
*/
cleanCropItemList () {
let cropItemList = this.cropItemList
// 1. 依据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无重叠,向列表添加一项,更新记录参数
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
}
четвертый шаг
Используйте инструмент обрезки: Связь между медиа и инструментами обрезки достигается с помощью реквизита и генерирования событий.
<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>
Суммировать
Писать блог намного сложнее, чем писать код, и мне кажется, что закончить писать этот блог — это беспорядок.
несколько мелких деталей
Анимация высоты при добавлении или удалении списка
Пользовательский интерфейс выдвигает требование отображать не более 10 вырезок, прокручивая более одного, а также добавляя и удаляя анимацию. Я думал, что прямойmax-height
Готово, оказывается
CSS.transition
Анимации действительны только для абсолютных высот., что немного хлопотно, потому что меняется количество полос обрезки, поэтому меняется и высота. Как установить абсолютное значение. . .
Здесь через тег в HTMLattribute
Атрибутыdata-count
чтобы сообщить CSS, что у меня есть несколько клипов, а затем позволить CSSdata-count
установить высоту списка.
<!--超过10条数据也只传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
Также существует проблема с тем, является ли это индикатором выполнения, потому что при перемещении мыши всегда следует отметка времени, в результате чего иногда соответствующий индикатор выполнения не запускается в течение определенного периода времени.mousemove
мероприятие.
Решение состоит в том, чтобы получить расстояние от далекой левой панели прогресса до далекой левой страницы после нагрузки страницы.mousemove
событие не принятоoffsetX
, и вместо этого возьмите страницу на основе крайнего левогоclientX
, а затем вычтите два, чтобы получить значение пикселя в крайнем левом углу индикатора выполнения от мыши. Добавление кода вышеmousemove
Мониторинг написан.
форматирование времени
Потому что инструмент для урожая должен преобразовать секунды до00:00:00
форматировать строку, поэтому я написал служебную функцию: ввод секунд, вывод содержащегоdd,HH,mm,ss
четыреkey
изObject
, каждыйkey
это строка длины 2. с ES8String.prototype.padStart()реализация метода.
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')
};
}