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

148 lines
4.0 KiB
TypeScript

// Web Worker for concurrent image frame downloading and decoding
interface FrameRequest {
url: string
frameIndex: number
priority: 'high' | 'normal' | 'low'
}
interface FrameResponse {
frameIndex: number
url: string
error?: string
priority?: 'high' | 'normal' | 'low'
}
interface WorkerMessage {
type: 'load' | 'prefetch' | 'clear' | 'setBuffer'
data?: any
}
class ImageFrameLoader {
private loadingQueue = new Set<number>()
private maxConcurrent = 4 // 最大并发下载数
private targetPrefetch = 4 // 预取帧数
private currentLoading = 0
constructor() {
this.setupMessageHandler()
}
private setupMessageHandler() {
self.addEventListener('message', async (event: MessageEvent<WorkerMessage>) => {
const { type, data } = event.data
switch (type) {
case 'load':
await this.loadFrame(data?.url, data?.frameIndex, data?.priority || 'high')
break
case 'prefetch':
await this.prefetchFrames(data?.urls, data?.startIndex)
break
case 'clear':
this.clearCache()
break
case 'setBuffer':
this.targetPrefetch = data?.targetPrefetch || 4
break
}
})
}
private async loadFrame(url: string, frameIndex: number, priority: 'high' | 'normal' | 'low') {
// 如果正在加载,跳过
if (this.loadingQueue.has(frameIndex)) {
return
}
// 检查并发限制
if (this.currentLoading >= this.maxConcurrent && priority !== 'high') {
// 对于非高优先级请求,延迟处理
setTimeout(() => this.loadFrame(url, frameIndex, priority), 50)
return
}
this.loadingQueue.add(frameIndex)
this.currentLoading++
try {
const imageBitmap = await this.fetchAndDecodeImage(url)
// 直接发送到主线程,不在 worker 中缓存
this.postFrameResponse(frameIndex, imageBitmap, url, priority)
} catch (error) {
// 区分优先级处理错误
if (priority === 'high') {
// 高优先级请求(当前帧)应该报告错误
this.postError(frameIndex, url, (error as Error).message)
} else {
console.debug(`Prefetch frame ${frameIndex} not available (${url}) - this is normal for future timestamps`)
}
} finally {
this.loadingQueue.delete(frameIndex)
this.currentLoading--
}
}
private async prefetchFrames(urls: string[], startIndex: number) {
const prefetchPromises: Promise<void>[] = []
// 并发预取接下来的 targetPrefetch 帧
for (let i = 0; i < this.targetPrefetch && i < urls.length; i++) {
const frameIndex = startIndex + i
const url = urls[i]
if (!this.loadingQueue.has(frameIndex)) {
prefetchPromises.push(this.loadFrame(url, frameIndex, 'normal'))
}
}
await Promise.allSettled(prefetchPromises)
}
private async fetchAndDecodeImage(url: string): Promise<ImageBitmap> {
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const blob = await response.blob()
// 使用 createImageBitmap 进行优化解码
const imageBitmap = await createImageBitmap(blob, {
colorSpaceConversion: 'none', // 不进行颜色空间转换
premultiplyAlpha: 'none', // 不预乘 alpha
resizeQuality: 'high' // 高质量缩放(如果需要)
})
return imageBitmap
}
private postFrameResponse(frameIndex: number, imageBitmap: ImageBitmap, url: string, priority?: 'high' | 'normal' | 'low') {
postMessage({
frameIndex,
imageBitmap,
url,
priority
}, { transfer: [imageBitmap] })
}
private postError(frameIndex: number, url: string, error: string) {
postMessage({
frameIndex,
url,
error
})
}
private clearCache() {
// 清理加载队列
this.loadingQueue.clear()
}
}
// 初始化 worker
new ImageFrameLoader()
export type { FrameRequest, FrameResponse, WorkerMessage }