mirror of
https://github.com/zhw2590582/ArtPlayer.git
synced 2026-10-25 08:01:20 -08:00
refactor(core): CORE-19 type capture and preserve progress lifetimes
This commit is contained in:
1 parent
98a1800032
commit
ccf77c4ed2
49 files changed
+31570
-681
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import type { MediaHost } from '../media/hosts'
|
||||
import type { Position } from '../media/playback'
|
||||
import { advancePosition } from '../media/position-revision'
|
||||
import { clamp, def } from '../utils'
|
||||
|
||||
export default function currentTimeMix(art: MediaHost<Pick<HTMLMediaElement, 'currentTime'>> & Pick<Position, 'duration'>): asserts art is typeof art & Position {
|
||||
@@ -11,6 +12,7 @@ export default function currentTimeMix(art: MediaHost<Pick<HTMLMediaElement, 'cu
|
||||
const parsed = Number.parseFloat(time as string)
|
||||
if (Number.isNaN(parsed))
|
||||
return
|
||||
advancePosition(art)
|
||||
$video.currentTime = clamp(parsed, 0, art.duration)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import { createElement, def, download, secondToTime } from '../utils'
|
||||
|
||||
export default function screenshotMix(art) {
|
||||
const {
|
||||
notice,
|
||||
template: { $video },
|
||||
} = art
|
||||
|
||||
const $canvas = createElement('canvas')
|
||||
|
||||
def(art, 'getDataURL', {
|
||||
value: () =>
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
$canvas.width = $video.videoWidth
|
||||
$canvas.height = $video.videoHeight
|
||||
$canvas.getContext('2d').drawImage($video, 0, 0)
|
||||
resolve($canvas.toDataURL('image/png'))
|
||||
}
|
||||
catch (err) {
|
||||
notice.show = err
|
||||
reject(err)
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
def(art, 'getBlobUrl', {
|
||||
value: () =>
|
||||
new Promise((resolve, reject) => {
|
||||
try {
|
||||
$canvas.width = $video.videoWidth
|
||||
$canvas.height = $video.videoHeight
|
||||
$canvas.getContext('2d').drawImage($video, 0, 0)
|
||||
$canvas.toBlob((blob) => {
|
||||
resolve(URL.createObjectURL(blob))
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
notice.show = err
|
||||
reject(err)
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
def(art, 'screenshot', {
|
||||
value: async (name) => {
|
||||
const dataUri = await art.getDataURL()
|
||||
const fileName = name || `artplayer_${secondToTime($video.currentTime)}`
|
||||
download(dataUri, `${fileName}.png`)
|
||||
art.emit('screenshot', dataUri)
|
||||
return dataUri
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { CaptureMedia } from '../capture/frame'
|
||||
import { captureBlobUrl, drawFrame } from '../capture/frame'
|
||||
import { isClosing } from '../lifecycle/instance'
|
||||
import { captureSource } from '../source/operation'
|
||||
import { def, download, secondToTime } from '../utils'
|
||||
|
||||
export interface ScreenshotHost {
|
||||
template: { $video: CaptureMedia }
|
||||
notice: { set show(value: unknown) }
|
||||
getDataURL: () => Promise<string>
|
||||
emit: (name: 'screenshot', dataUri: string) => unknown
|
||||
}
|
||||
|
||||
export default function screenshotMix(art: ScreenshotHost): void {
|
||||
const { notice, template: { $video } } = art
|
||||
const canvas = document.createElement('canvas')
|
||||
|
||||
function capture(blob: boolean): Promise<string> {
|
||||
const active = captureSource(art)
|
||||
// Draw synchronously as before; awaiting must not capture a later frame.
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const fail = (error: unknown) => {
|
||||
try {
|
||||
if (!isClosing(art) && active())
|
||||
notice.show = error
|
||||
}
|
||||
catch (noticeError) {
|
||||
reject(noticeError)
|
||||
return
|
||||
}
|
||||
reject(error)
|
||||
}
|
||||
try {
|
||||
drawFrame(canvas, $video)
|
||||
if (blob) {
|
||||
// Handle failures inside toBlob as well as synchronous canvas errors.
|
||||
captureBlobUrl(canvas, resolve, fail)
|
||||
}
|
||||
else {
|
||||
resolve(canvas.toDataURL('image/png'))
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
fail(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
def(art, 'getDataURL', { value: () => capture(false) })
|
||||
def(art, 'getBlobUrl', { value: () => capture(true) })
|
||||
def(art, 'screenshot', {
|
||||
value: async (name?: string): Promise<string> => {
|
||||
const active = captureSource(art)
|
||||
const dataUri = await art.getDataURL()
|
||||
if (!isClosing(art) && active()) {
|
||||
const fileName = name || `artplayer_${secondToTime($video.currentTime)}`
|
||||
if (!isClosing(art) && active())
|
||||
download(dataUri, `${fileName}.png`)
|
||||
if (!isClosing(art) && active())
|
||||
art.emit('screenshot', dataUri)
|
||||
}
|
||||
return dataUri
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import { def, isMobile, loadImg, setStyle } from '../utils'
|
||||
|
||||
export default function thumbnailsMix(art) {
|
||||
const {
|
||||
option,
|
||||
template: { $progress, $video },
|
||||
} = art
|
||||
|
||||
let timer = null
|
||||
let loding = false
|
||||
let image = null
|
||||
|
||||
function reset() {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
loding = false
|
||||
image = null
|
||||
}
|
||||
|
||||
function showThumbnails(posWidth) {
|
||||
const $thumbnails = art.controls?.thumbnails
|
||||
if (!$thumbnails)
|
||||
return
|
||||
|
||||
const { number, column, width, height, scale } = option.thumbnails
|
||||
const width2 = width * scale || image.naturalWidth / column
|
||||
const height2 = height * scale || width2 / ($video.videoWidth / $video.videoHeight)
|
||||
const perWidth = $progress.clientWidth / number
|
||||
const perIndex = Math.floor(posWidth / perWidth)
|
||||
const yIndex = Math.ceil(perIndex / column) - 1
|
||||
const xIndex = perIndex % column || column - 1
|
||||
setStyle($thumbnails, 'backgroundImage', `url(${image.src})`)
|
||||
setStyle($thumbnails, 'height', `${height2}px`)
|
||||
setStyle($thumbnails, 'width', `${width2}px`)
|
||||
setStyle($thumbnails, 'backgroundPosition', `-${xIndex * width2}px -${yIndex * height2}px`)
|
||||
if (posWidth <= width2 / 2) {
|
||||
setStyle($thumbnails, 'left', 0)
|
||||
}
|
||||
else if (posWidth > $progress.clientWidth - width2 / 2) {
|
||||
setStyle($thumbnails, 'left', `${$progress.clientWidth - width2}px`)
|
||||
}
|
||||
else {
|
||||
setStyle($thumbnails, 'left', `${posWidth - width2 / 2}px`)
|
||||
}
|
||||
}
|
||||
|
||||
art.on('setBar', async (type, percentage, event) => {
|
||||
const $thumbnails = art.controls?.thumbnails
|
||||
const { url, scale } = option.thumbnails
|
||||
if (!$thumbnails || !url)
|
||||
return
|
||||
|
||||
const isMobileDragging = type === 'played' && event && isMobile
|
||||
|
||||
if (type === 'hover' || isMobileDragging) {
|
||||
if (!image && !loding) {
|
||||
loding = true
|
||||
image = await loadImg(url, scale)
|
||||
loding = false
|
||||
}
|
||||
|
||||
if (!image)
|
||||
return
|
||||
|
||||
const width = $progress.clientWidth * percentage
|
||||
|
||||
if (width > 0 && width < $progress.clientWidth) {
|
||||
showThumbnails(width)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
def(art, 'thumbnails', {
|
||||
get() {
|
||||
return art.option.thumbnails
|
||||
},
|
||||
set(thumbnails) {
|
||||
if (thumbnails.url && !art.option.isLive) {
|
||||
art.option.thumbnails = thumbnails
|
||||
reset()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { Thumbnails } from '../../types/option'
|
||||
import type { SubscriptionHost } from '../component/resources'
|
||||
import { entryScope } from '../component/resources'
|
||||
import { eventSubscriptions } from '../events/subscriptions'
|
||||
import { loadThumbnailImage } from '../image/load'
|
||||
import { getScope, isClosing } from '../lifecycle/instance'
|
||||
import { captureSource } from '../source/operation'
|
||||
import { thumbnailLayout } from '../thumbnails/layout'
|
||||
import { def, isMobile, setStyle } from '../utils'
|
||||
|
||||
interface ThumbnailEvents {
|
||||
setBar: [type: string, percentage: number, event?: Event]
|
||||
}
|
||||
|
||||
export interface ThumbnailsHost extends SubscriptionHost<ThumbnailEvents> {
|
||||
option: { thumbnails: Thumbnails, isLive: boolean }
|
||||
template: { $progress: HTMLElement, $video: { videoWidth: number, videoHeight: number } }
|
||||
controls?: { thumbnails?: HTMLDivElement }
|
||||
}
|
||||
|
||||
export default function thumbnailsMix(art: ThumbnailsHost): void {
|
||||
const { option, template: { $progress, $video } } = art
|
||||
let scope = getScope(art).child()
|
||||
let image: HTMLImageElement | undefined
|
||||
let loading = false
|
||||
let control: HTMLDivElement | undefined
|
||||
let hover: { percentage: number, element: HTMLDivElement, sourceActive: () => boolean } | undefined
|
||||
|
||||
function reset(): void {
|
||||
const previous = scope
|
||||
scope = getScope(art).child()
|
||||
image = undefined
|
||||
loading = false
|
||||
hover = undefined
|
||||
previous.dispose()
|
||||
}
|
||||
|
||||
function render(): void {
|
||||
const current = hover
|
||||
const currentScope = scope
|
||||
if (!current || !image)
|
||||
return
|
||||
const active = () => !isClosing(art) && !currentScope.closed && scope === currentScope
|
||||
&& hover === current && current.sourceActive() && art.controls?.thumbnails === current.element
|
||||
if (!active())
|
||||
return
|
||||
const position = $progress.clientWidth * current.percentage
|
||||
if (!(position > 0 && position < $progress.clientWidth))
|
||||
return
|
||||
const styles = {
|
||||
backgroundImage: `url(${image.src})`,
|
||||
...thumbnailLayout(option.thumbnails, {
|
||||
imageWidth: image.naturalWidth,
|
||||
videoWidth: $video.videoWidth,
|
||||
videoHeight: $video.videoHeight,
|
||||
progressWidth: $progress.clientWidth,
|
||||
position,
|
||||
}),
|
||||
}
|
||||
for (const [key, value] of Object.entries(styles)) {
|
||||
if (!active())
|
||||
return
|
||||
setStyle(current.element, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
eventSubscriptions<ThumbnailEvents>(art)('setBar', (type, percentage, event) => {
|
||||
const element = art.controls?.thumbnails
|
||||
const { url, scale } = option.thumbnails
|
||||
if (isClosing(art) || !element || !url || !(type === 'hover' || (type === 'played' && event && isMobile)))
|
||||
return
|
||||
if (control !== element) {
|
||||
const elementScope = entryScope(element)
|
||||
if (elementScope.closed)
|
||||
return
|
||||
control = element
|
||||
elementScope.add(() => {
|
||||
if (control === element) {
|
||||
control = undefined
|
||||
reset()
|
||||
}
|
||||
})
|
||||
}
|
||||
hover = { percentage, element, sourceActive: captureSource(art) }
|
||||
if (image) {
|
||||
render()
|
||||
}
|
||||
else if (!loading) {
|
||||
const currentScope = scope
|
||||
loading = true
|
||||
loadThumbnailImage(url, scale, currentScope).then((loaded) => {
|
||||
if (scope !== currentScope || currentScope.closed)
|
||||
return
|
||||
loading = false
|
||||
image = loaded
|
||||
render()
|
||||
}).catch((error: unknown) => {
|
||||
if (scope === currentScope && !currentScope.closed) {
|
||||
loading = false
|
||||
console.warn('ArtPlayer thumbnail load failed:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
def(art, 'thumbnails', {
|
||||
get: () => art.option.thumbnails,
|
||||
set: (thumbnails: Thumbnails) => {
|
||||
if (!isClosing(art) && thumbnails.url && !art.option.isLive && !isClosing(art)) {
|
||||
art.option.thumbnails = thumbnails
|
||||
reset()
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -50,8 +50,7 @@ export default function urlMix<Media extends SourceMedia, Host extends UrlHost<M
|
||||
void Promise.resolve(result).catch(error => failSource(operation, error))
|
||||
}
|
||||
else {
|
||||
if (oldUrl)
|
||||
URL.revokeObjectURL(oldUrl)
|
||||
// Media URLs are supplied by callers or adapters, which own revocation.
|
||||
if (!operation.active())
|
||||
return
|
||||
$video.src = newUrl
|
||||
|
||||
Reference in new issue
Block a user