mosaicmap/hooks/use-image-frame-loader.ts
2025-08-26 11:42:24 +08:00

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
}
}