185 lines
5.1 KiB
TypeScript
185 lines
5.1 KiB
TypeScript
import { useRef, useCallback, useState, useMemo } from 'react'
|
|
import { LRUCache } from 'lru-cache'
|
|
|
|
interface UseImageFrameLoaderOptions {
|
|
maxBufferSize?: number
|
|
targetPrefetch?: number
|
|
}
|
|
|
|
interface FrameLoadedCallback {
|
|
(frameIndex: number, imageBitmap: ImageBitmap): void
|
|
}
|
|
|
|
export function useImageFrameLoader(options: UseImageFrameLoaderOptions = {}) {
|
|
const frameCache = useRef<LRUCache<number, ImageBitmap>>(new LRUCache({
|
|
max: options.maxBufferSize || 12,
|
|
dispose: (bitmap) => {
|
|
bitmap.close() // 释放 ImageBitmap 内存
|
|
}
|
|
}))
|
|
const [isLoading, setIsLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
// 订阅者管理
|
|
const frameSubscribers = useRef<Map<number, Set<FrameLoadedCallback>>>(new Map())
|
|
|
|
// 初始化 Worker
|
|
const worker = useMemo(() => {
|
|
if (typeof window !== 'undefined' && window.Worker) {
|
|
try {
|
|
const worker = new Worker(
|
|
new URL('../workers/image-frame-loader.ts', import.meta.url),
|
|
{ type: 'module' }
|
|
)
|
|
|
|
// 监听 Worker 消息
|
|
worker.addEventListener('message', (event) => {
|
|
const { frameIndex, error, imageBitmap, priority } = event.data
|
|
|
|
if (error) {
|
|
setError(error)
|
|
} else if (imageBitmap) {
|
|
debugger
|
|
frameCache.current.set(frameIndex, imageBitmap)
|
|
|
|
// 如果是高优先级请求,通知订阅者
|
|
if (priority === 'high') {
|
|
const subscribers = frameSubscribers.current.get(frameIndex)
|
|
if (subscribers) {
|
|
subscribers.forEach(callback => {
|
|
try {
|
|
callback(frameIndex, imageBitmap)
|
|
} catch (err) {
|
|
console.error('Frame subscriber callback error:', err)
|
|
}
|
|
})
|
|
// 清理订阅者
|
|
frameSubscribers.current.delete(frameIndex)
|
|
}
|
|
}
|
|
}
|
|
|
|
setIsLoading(false)
|
|
})
|
|
|
|
// 设置预取配置
|
|
worker.postMessage({
|
|
type: 'setBuffer',
|
|
data: {
|
|
targetPrefetch: options.targetPrefetch || 4
|
|
}
|
|
})
|
|
|
|
return worker
|
|
|
|
} catch (err) {
|
|
console.error('Failed to initialize image frame loader worker:', err)
|
|
setError('Worker initialization failed')
|
|
return null
|
|
}
|
|
} else {
|
|
setError('Web Workers not supported')
|
|
return null
|
|
}
|
|
|
|
}, [options.maxBufferSize, options.targetPrefetch]) // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
// 加载单个帧
|
|
const loadFrame = useCallback((url: string, frameIndex: number, priority: 'high' | 'normal' | 'low' = 'high') => {
|
|
if (!worker) {
|
|
setError('Worker not available')
|
|
return
|
|
}
|
|
|
|
// 如果主线程缓存中已存在,直接返回
|
|
if (frameCache.current.has(frameIndex)) {
|
|
return
|
|
}
|
|
|
|
setIsLoading(true)
|
|
setError(null)
|
|
|
|
worker.postMessage({
|
|
type: 'load',
|
|
data: { url, frameIndex, priority }
|
|
})
|
|
}, [worker])
|
|
|
|
// 订阅帧加载完成通知
|
|
const subscribeToFrame = useCallback((frameIndex: number, callback: FrameLoadedCallback) => {
|
|
if (!frameSubscribers.current.has(frameIndex)) {
|
|
frameSubscribers.current.set(frameIndex, new Set())
|
|
}
|
|
frameSubscribers.current.get(frameIndex)!.add(callback)
|
|
|
|
// 返回取消订阅函数
|
|
return () => {
|
|
const subscribers = frameSubscribers.current.get(frameIndex)
|
|
if (subscribers) {
|
|
subscribers.delete(callback)
|
|
if (subscribers.size === 0) {
|
|
frameSubscribers.current.delete(frameIndex)
|
|
}
|
|
}
|
|
}
|
|
}, [])
|
|
|
|
// 预取多个帧
|
|
const prefetchFrames = useCallback((urls: string[], startIndex: number) => {
|
|
if (!worker) {
|
|
setError('Worker not available')
|
|
return
|
|
}
|
|
|
|
worker.postMessage({
|
|
type: 'prefetch',
|
|
data: { urls, startIndex }
|
|
})
|
|
}, [])
|
|
|
|
// 清理缓存
|
|
const clearCache = useCallback(() => {
|
|
if (!worker) return
|
|
|
|
// LRU 缓存会自动调用 dispose 方法释放 ImageBitmap
|
|
frameCache.current.clear()
|
|
worker.postMessage({ type: 'clear' })
|
|
}, [worker])
|
|
|
|
// 获取帧 - 直接从 LRU 缓存获取
|
|
const getFrame = useCallback((frameIndex: number): ImageBitmap | null => {
|
|
console.log(Array.from(frameCache.current.keys()))
|
|
return frameCache.current.get(frameIndex) || null
|
|
}, [])
|
|
|
|
// 检查帧是否已缓存
|
|
const isFrameCached = useCallback((frameIndex: number): boolean => {
|
|
return frameCache.current.has(frameIndex)
|
|
}, [])
|
|
|
|
// 获取缓存统计信息
|
|
const getCacheStats = useCallback(() => {
|
|
const cache = frameCache.current
|
|
const totalFrames = cache.size
|
|
const maxFrames = cache.max
|
|
|
|
return {
|
|
totalFrames,
|
|
maxFrames,
|
|
cacheUtilization: maxFrames > 0 ? totalFrames / maxFrames : 0
|
|
}
|
|
}, [])
|
|
|
|
return {
|
|
loadFrame,
|
|
prefetchFrames,
|
|
clearCache,
|
|
getFrame,
|
|
isFrameCached,
|
|
getCacheStats,
|
|
subscribeToFrame,
|
|
isLoading,
|
|
error,
|
|
isWorkerSupported: typeof window !== 'undefined' && !!window.Worker
|
|
}
|
|
} |