mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-22 13:21:20 -08:00
refactor(vtt-thumbnail): [PKG-VTT-THUMB-03] own pending requests and preview resources
This commit is contained in:
1 parent
05244d1104
commit
524ddf784f
23 files changed
+3045
-262
No files matched your search
@@ -1,70 +1,6 @@
|
||||
function padEnd(str, targetLength, padString) {
|
||||
if (str.length > targetLength) {
|
||||
return String(str)
|
||||
}
|
||||
else {
|
||||
targetLength = targetLength - str.length
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length)
|
||||
}
|
||||
return String(str) + padString.slice(0, targetLength)
|
||||
}
|
||||
}
|
||||
|
||||
function t2d(time) {
|
||||
const arr = time.split('.')
|
||||
const left = arr[0].split(':') || []
|
||||
const right = padEnd(arr[1] || '0', 3, '0')
|
||||
const ms = Number(right) / 1000
|
||||
|
||||
const h = Number(left[left.length - 3] || 0) * 3600
|
||||
const m = Number(left[left.length - 2] || 0) * 60
|
||||
const s = Number(left[left.length - 1] || 0)
|
||||
return h + m + s + ms
|
||||
}
|
||||
import parseVtt from './parseVtt'
|
||||
|
||||
// Preserve the historical source helper while registration owns its requests.
|
||||
export default async function getVttArray(vttUrl = '') {
|
||||
const vttString = await (await fetch(vttUrl)).text()
|
||||
const lines = vttString.split(/[\n\r]/g).filter(item => item.trim())
|
||||
const vttArray = []
|
||||
|
||||
for (let i = 1; i < lines.length; i += 2) {
|
||||
const time = lines[i]
|
||||
const text = lines[i + 1]
|
||||
|
||||
if (!text.trim())
|
||||
continue
|
||||
|
||||
const timeReg
|
||||
= /((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?) ?--> ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/
|
||||
const timeMatch = time.match(timeReg)
|
||||
|
||||
const textReg = /(.*)#(\w{4})=(.*)/
|
||||
const textMatch = text.match(textReg)
|
||||
|
||||
const start = Math.floor(t2d(timeMatch[1]))
|
||||
const end = Math.floor(t2d(timeMatch[2]))
|
||||
|
||||
let url = textMatch[1]
|
||||
const isAbsoluteUrl = /^\/|(?:https?|ftp|file):\/\//i.test(url)
|
||||
if (!isAbsoluteUrl) {
|
||||
const urlArr = vttUrl.split('/')
|
||||
urlArr.pop()
|
||||
urlArr.push(url)
|
||||
url = urlArr.join('/')
|
||||
}
|
||||
|
||||
const result = { start, end, url }
|
||||
|
||||
const keys = textMatch[2].split('')
|
||||
const values = textMatch[3].split(',')
|
||||
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
result[keys[j]] = values[j]
|
||||
}
|
||||
|
||||
vttArray.push(result)
|
||||
}
|
||||
|
||||
return vttArray
|
||||
return parseVtt(await (await fetch(vttUrl)).text(), vttUrl)
|
||||
}
|
||||
@@ -1,74 +1,46 @@
|
||||
import getVttArray from './getVttArray'
|
||||
import createLifetime from './lifetime'
|
||||
import parseVtt from './parseVtt'
|
||||
import createPreview from './preview'
|
||||
import requestVtt from './request'
|
||||
|
||||
export default function artplayerPluginVttThumbnail(option) {
|
||||
return async (art) => {
|
||||
const {
|
||||
constructor: {
|
||||
utils: { setStyle, isMobile, addClass },
|
||||
},
|
||||
template: { $progress },
|
||||
} = art
|
||||
|
||||
let timer = null
|
||||
const thumbnails = await getVttArray(option.vtt)
|
||||
|
||||
function showThumbnails($control, find, width) {
|
||||
setStyle($control, 'backgroundImage', `url(${find.url})`)
|
||||
setStyle($control, 'height', `${find.h}px`)
|
||||
setStyle($control, 'width', `${find.w}px`)
|
||||
setStyle($control, 'backgroundPosition', `-${find.x}px -${find.y}px`)
|
||||
if (width <= find.w / 2) {
|
||||
setStyle($control, 'left', 0)
|
||||
}
|
||||
else if (width > $progress.clientWidth - find.w / 2) {
|
||||
setStyle($control, 'left', `${$progress.clientWidth - find.w}px`)
|
||||
}
|
||||
else {
|
||||
setStyle($control, 'left', `${width - find.w / 2}px`)
|
||||
}
|
||||
const { constructor: { utils: { setStyle, isMobile, addClass } }, template: { $progress } } = art
|
||||
const lifetime = createLifetime(art)
|
||||
const result = { name: 'artplayerPluginVttThumbnail' }
|
||||
try {
|
||||
if (lifetime.closed)
|
||||
return result
|
||||
const url = option.vtt
|
||||
const text = await requestVtt(url, lifetime)
|
||||
if (lifetime.closed)
|
||||
return result
|
||||
const thumbnails = parseVtt(text, url)
|
||||
const preview = createPreview({ lifetime, thumbnails, progress: $progress, duration: () => art.duration, setStyle, isMobile })
|
||||
const style = option.style || {}
|
||||
if (lifetime.closed)
|
||||
return result
|
||||
art.controls.add({
|
||||
name: 'vtt-thumbnail',
|
||||
position: 'top',
|
||||
index: 20,
|
||||
style,
|
||||
mounted(control) {
|
||||
lifetime.own(() => {
|
||||
if (art.controls['vtt-thumbnail'] === control)
|
||||
art.controls.remove('vtt-thumbnail')
|
||||
})
|
||||
if (lifetime.closed)
|
||||
return
|
||||
addClass(control, 'art-control-thumbnails')
|
||||
lifetime.listen('setBar', preview(control))
|
||||
},
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
art.controls.add({
|
||||
name: 'vtt-thumbnail',
|
||||
position: 'top',
|
||||
index: 20,
|
||||
style: option.style || {},
|
||||
mounted($control) {
|
||||
addClass($control, 'art-control-thumbnails')
|
||||
art.on('setBar', async (type, percentage, event) => {
|
||||
const isMobileDroging = type === 'played' && event && isMobile
|
||||
|
||||
if (type === 'hover' || isMobileDroging) {
|
||||
const width = $progress.clientWidth * percentage
|
||||
const second = percentage * art.duration
|
||||
setStyle($control, 'display', 'flex')
|
||||
|
||||
const find = thumbnails.find(item => second >= item.start && second <= item.end)
|
||||
if (!find)
|
||||
return setStyle($control, 'display', 'none')
|
||||
|
||||
if (width > 0 && width < $progress.clientWidth) {
|
||||
showThumbnails($control, find, width)
|
||||
}
|
||||
else {
|
||||
if (!isMobile) {
|
||||
setStyle($control, 'display', 'none')
|
||||
}
|
||||
}
|
||||
|
||||
if (isMobileDroging) {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
setStyle($control, 'display', 'none')
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
name: 'artplayerPluginVttThumbnail',
|
||||
catch (error) {
|
||||
lifetime.dispose()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
export default function createLifetime(art) {
|
||||
let closed = Boolean(art.isDestroy)
|
||||
const cleanups = new Set()
|
||||
let cancel
|
||||
const cancelled = new Promise((resolve) => {
|
||||
cancel = resolve
|
||||
})
|
||||
const lifetime = {
|
||||
get closed() { return closed },
|
||||
own(cleanup) {
|
||||
if (closed)
|
||||
run(cleanup)
|
||||
else
|
||||
cleanups.add(cleanup)
|
||||
return () => cleanups.delete(cleanup)
|
||||
},
|
||||
listen(name, callback) {
|
||||
if (closed)
|
||||
return
|
||||
lifetime.own(() => art.off(name, callback))
|
||||
art.on(name, callback)
|
||||
},
|
||||
wait(value) { return Promise.race([value, cancelled]) },
|
||||
dispose() {
|
||||
if (closed)
|
||||
return
|
||||
closed = true
|
||||
cancel()
|
||||
const pending = [...cleanups].reverse()
|
||||
cleanups.clear()
|
||||
for (const cleanup of pending)
|
||||
run(cleanup)
|
||||
},
|
||||
}
|
||||
function run(cleanup) {
|
||||
try {
|
||||
cleanup()
|
||||
}
|
||||
catch (error) {
|
||||
console.warn('Failed to clean up VTT thumbnails:', error)
|
||||
}
|
||||
}
|
||||
try {
|
||||
lifetime.listen('destroy', lifetime.dispose)
|
||||
}
|
||||
catch (error) {
|
||||
lifetime.dispose()
|
||||
throw error
|
||||
}
|
||||
return lifetime
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
function padEnd(str, targetLength, padString) {
|
||||
if (str.length > targetLength) {
|
||||
return String(str)
|
||||
}
|
||||
else {
|
||||
targetLength = targetLength - str.length
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length)
|
||||
}
|
||||
return String(str) + padString.slice(0, targetLength)
|
||||
}
|
||||
}
|
||||
|
||||
function t2d(time) {
|
||||
const arr = time.split('.')
|
||||
const left = arr[0].split(':') || []
|
||||
const right = padEnd(arr[1] || '0', 3, '0')
|
||||
const ms = Number(right) / 1000
|
||||
|
||||
const h = Number(left[left.length - 3] || 0) * 3600
|
||||
const m = Number(left[left.length - 2] || 0) * 60
|
||||
const s = Number(left[left.length - 1] || 0)
|
||||
return h + m + s + ms
|
||||
}
|
||||
|
||||
export default function parseVtt(vttString, vttUrl = '') {
|
||||
const lines = vttString.split(/[\n\r]/g).filter(item => item.trim())
|
||||
const vttArray = []
|
||||
|
||||
for (let i = 1; i < lines.length; i += 2) {
|
||||
const time = lines[i]
|
||||
const text = lines[i + 1]
|
||||
|
||||
if (!text.trim())
|
||||
continue
|
||||
|
||||
const timeReg
|
||||
= /((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?) ?--> ?((?:\d{2}:)?(?:\d{2}:)?\d{2}(?:.\d{3})?)/
|
||||
const timeMatch = time.match(timeReg)
|
||||
|
||||
const textReg = /(.*)#(\w{4})=(.*)/
|
||||
const textMatch = text.match(textReg)
|
||||
|
||||
const start = Math.floor(t2d(timeMatch[1]))
|
||||
const end = Math.floor(t2d(timeMatch[2]))
|
||||
|
||||
let url = textMatch[1]
|
||||
const isAbsoluteUrl = /^\/|(?:https?|ftp|file):\/\//i.test(url)
|
||||
if (!isAbsoluteUrl) {
|
||||
const urlArr = vttUrl.split('/')
|
||||
urlArr.pop()
|
||||
urlArr.push(url)
|
||||
url = urlArr.join('/')
|
||||
}
|
||||
|
||||
const result = { start, end, url }
|
||||
|
||||
const keys = textMatch[2].split('')
|
||||
const values = textMatch[3].split(',')
|
||||
|
||||
for (let j = 0; j < keys.length; j++) {
|
||||
result[keys[j]] = values[j]
|
||||
}
|
||||
|
||||
vttArray.push(result)
|
||||
}
|
||||
|
||||
return vttArray
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
export default function createPreview({ lifetime, thumbnails, progress, duration, setStyle, isMobile }) {
|
||||
let timer = null
|
||||
let generation = 0
|
||||
lifetime.own(() => {
|
||||
generation++
|
||||
if (timer !== null) {
|
||||
const previous = timer
|
||||
timer = null
|
||||
clearTimeout(previous)
|
||||
}
|
||||
})
|
||||
function style(control, key, value) {
|
||||
if (!lifetime.closed)
|
||||
setStyle(control, key, value)
|
||||
}
|
||||
function show(control, cue, width) {
|
||||
style(control, 'backgroundImage', `url(${cue.url})`)
|
||||
style(control, 'height', `${cue.h}px`)
|
||||
style(control, 'width', `${cue.w}px`)
|
||||
style(control, 'backgroundPosition', `-${cue.x}px -${cue.y}px`)
|
||||
if (width <= cue.w / 2)
|
||||
style(control, 'left', 0)
|
||||
else if (width > progress.clientWidth - cue.w / 2)
|
||||
style(control, 'left', `${progress.clientWidth - cue.w}px`)
|
||||
else
|
||||
style(control, 'left', `${width - cue.w / 2}px`)
|
||||
}
|
||||
return control => async (type, percentage, event) => {
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const dragging = type === 'played' && event && isMobile
|
||||
if (type !== 'hover' && !dragging)
|
||||
return
|
||||
const width = progress.clientWidth * percentage
|
||||
const second = percentage * duration()
|
||||
style(control, 'display', 'flex')
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const cue = thumbnails.find(item => second >= item.start && second <= item.end)
|
||||
if (!cue)
|
||||
return style(control, 'display', 'none')
|
||||
if (width > 0 && width < progress.clientWidth)
|
||||
show(control, cue, width)
|
||||
else if (!isMobile)
|
||||
style(control, 'display', 'none')
|
||||
if (dragging && !lifetime.closed) {
|
||||
const current = ++generation
|
||||
if (timer !== null)
|
||||
clearTimeout(timer)
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const id = setTimeout(() => {
|
||||
if (current !== generation || lifetime.closed)
|
||||
return
|
||||
timer = null
|
||||
style(control, 'display', 'none')
|
||||
}, 500)
|
||||
if (lifetime.closed || current !== generation)
|
||||
clearTimeout(id)
|
||||
else
|
||||
timer = id
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
export default async function requestVtt(url = '', lifetime) {
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const controller = typeof AbortController === 'function' ? new AbortController() : null
|
||||
const release = lifetime.own(() => controller?.abort())
|
||||
try {
|
||||
if (lifetime.closed)
|
||||
return
|
||||
const response = await lifetime.wait(controller ? fetch(url, { signal: controller.signal }) : fetch(url))
|
||||
if (lifetime.closed)
|
||||
return
|
||||
if (response.ok === false)
|
||||
throw new Error(`Failed to fetch VTT thumbnails: HTTP ${response.status}`)
|
||||
if (lifetime.closed)
|
||||
return
|
||||
return await lifetime.wait(response.text())
|
||||
}
|
||||
finally {
|
||||
release()
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user