Skip to content

Commit d148231

Browse files
committed
feat(getVideoFrame): add function to extract a frame from a video file at a specified time
1 parent 56fd96b commit d148231

File tree

3 files changed

+83
-6
lines changed

3 files changed

+83
-6
lines changed

src/event/useEventListener.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { mount } from '../utils/mount'
22
import { unmount } from '../utils/unmount'
3+
import { useMutationObserver } from './useMutationObserver'
34

45
/**
56
* 事件监听
@@ -54,12 +55,26 @@ export function useEventListener<
5455
event(e as (WindowEventMap & DocumentEventMap)[T])
5556
}
5657
target.addEventListener(eventName, eventFunction, useCapture)
57-
stop = () =>
58-
(target as Element).removeEventListener(
59-
eventName,
60-
eventFunction,
61-
useCapture,
62-
)
58+
59+
let mutationStop: (() => void) | undefined
60+
// 仅对 Element 类型做 DOM 移除监听
61+
if (target instanceof Element && target.parentNode) {
62+
mutationStop = useMutationObserver(target.parentNode, (mutations) => {
63+
for (const mutation of mutations) {
64+
for (const node of Array.from(mutation.removedNodes)) {
65+
if (node === target) {
66+
stop?.()
67+
mutationStop?.()
68+
}
69+
}
70+
}
71+
})
72+
}
73+
74+
stop = () => {
75+
target.removeEventListener(eventName, eventFunction, useCapture)
76+
mutationStop?.()
77+
}
6378
})
6479
return () => {
6580
if (!stop)

src/screen/getVideoFrame.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { Canvas } from '../canvas'
2+
3+
/**
4+
* 从视频文件中获取指定时间帧的图片
5+
* @param file 视频类型的File对象
6+
* @param time 需要截取帧的时间(单位:秒)
7+
* @returns Promise<{ url: string; blob: Blob }>
8+
*/
9+
export async function getVideoFrame(
10+
file: File,
11+
time: number = 0,
12+
): Promise<{ url: string, blob: Blob }> {
13+
if (!file.type.startsWith('video/')) {
14+
throw new Error('文件类型不是视频')
15+
}
16+
17+
return new Promise((resolve, reject) => {
18+
const video = document.createElement('video')
19+
const url = URL.createObjectURL(file)
20+
video.src = url
21+
video.muted = true
22+
video.autoplay = false
23+
24+
video.onloadedmetadata = () => {
25+
if (time > video.duration) {
26+
URL.revokeObjectURL(url)
27+
reject(new Error('指定时间超出视频时长'))
28+
return
29+
}
30+
video.currentTime = time
31+
}
32+
33+
video.onseeked = () => {
34+
const width = video.videoWidth
35+
const height = video.videoHeight
36+
if (!width || !height) {
37+
URL.revokeObjectURL(url)
38+
reject(new Error('无法获取视频尺寸'))
39+
return
40+
}
41+
const { canvas, ctx } = new Canvas()
42+
canvas.width = width
43+
canvas.height = height
44+
ctx.drawImage(video, 0, 0, width, height)
45+
canvas.toBlob((blob) => {
46+
URL.revokeObjectURL(url)
47+
if (!blob) {
48+
reject(new Error('生成图片失败'))
49+
return
50+
}
51+
const frameUrl = URL.createObjectURL(blob)
52+
resolve({ url: frameUrl, blob })
53+
}, 'image/png')
54+
}
55+
56+
video.onerror = () => {
57+
URL.revokeObjectURL(url)
58+
reject(new Error('视频加载失败'))
59+
}
60+
})
61+
}

src/screen/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ export * from './useRecorder'
99
export * from './useVideoSubtitle'
1010
export * from './useAudio'
1111
export * from './useFrequency'
12+
export * from './getVideoFrame'

0 commit comments

Comments
 (0)