UNPKG

svga-web

Version:

A SVGA player for modern Web.

190 lines (163 loc) 5.28 kB
import render from './offscreen.canvas.render' import { com } from '../proto/svga' import VideoEntity, { DynamicElements, ImageSources, Sprite, } from '../parser/video-entity' import svga = com.opensource.svga interface AudioConfig extends svga.AudioEntity { audio: HTMLAudioElement } function polyfillCreateImageBitmap() { /* Safari and Edge polyfill for createImageBitmap * https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/createImageBitmap */ if (!('createImageBitmap' in window)) { Object.assign(window, { createImageBitmap: async function (blob: Blob) { return new Promise((resolve, reject) => { const img = document.createElement('img') img.addEventListener('load', function () { resolve(this) }) img.addEventListener('error', function (e) { // console.log('createImageBitmap error', e); reject(e) }) img.src = URL.createObjectURL(blob) }) }, }) } } export default class Renderer { private readonly target: HTMLCanvasElement private audios: HTMLAudioElement[] = [] private audioConfigs: { [frame: number]: AudioConfig[] | undefined } = {} isCacheFrame = false private readonly frameCache: { [frame: number]: ImageBitmap } = {} private readonly offscreenCanvas: HTMLCanvasElement | OffscreenCanvas constructor(target: HTMLCanvasElement) { polyfillCreateImageBitmap() this.target = target this.offscreenCanvas = window.OffscreenCanvas ? new window.OffscreenCanvas(target.width, target.height) : document.createElement('canvas') } public async prepare(videoItem: VideoEntity): Promise<void> { this.audios = [] this.audioConfigs = {} // 重新设置 canvas 的尺寸,哪怕设置的值与原值没有区别,都会导致 canvas 重绘,在移动端上会清屏 https://blog.csdn.net/harmsworth2016/article/details/118426390 if (this.target.width !== videoItem.videoSize.width) { this.target.width = videoItem.videoSize.width } if (this.target.height !== videoItem.videoSize.height) { this.target.height = videoItem.videoSize.height } const addAudioConfig = (frame: number, ac: AudioConfig) => { const acs = this.audioConfigs[frame] || [] acs.push(ac) this.audioConfigs[frame] = acs } const loadImages = Object.entries(videoItem.images).map( async ([key, item]) => { if (item instanceof ArrayBuffer) { const blob = new Blob([item], { type: 'image/png' }) const bitmap = await createImageBitmap(blob) videoItem.images[key] = bitmap } return item }, ) const loadAudios = Object.values(videoItem.audios).map( ({ source, startFrame, endFrame, audioKey, startTime, totalTime }) => new Promise((resolve) => { const audio = new Audio( URL.createObjectURL( new Blob([new Uint8Array(source)], { type: 'audio/x-mpeg' }), ), ) const ac: AudioConfig = { audioKey, audio, startFrame, endFrame, startTime, totalTime, } addAudioConfig(startFrame, ac) addAudioConfig(endFrame, ac) this.audios.push(audio) audio.onloadeddata = resolve audio.load() }), ) await Promise.all([...loadAudios, ...loadImages]) } public processAudio(frame: number): void { const acs = this.audioConfigs[frame] if (!acs || acs.length === 0) { return } acs.forEach(function (ac) { if (ac.startFrame === frame) { ac.audio.currentTime = ac.startTime ac.audio.play() return } if (ac.endFrame === frame) { ac.audio.pause() ac.audio.currentTime = 0 return } }) } public clear(): void { const context2d = this.target.getContext('2d') context2d?.clearRect(0, 0, this.target.width, this.target.height) } public drawFrame( images: ImageSources, sprites: Array<Sprite>, dynamicElements: DynamicElements, frame: number, ): void { const context2d = this.target.getContext('2d') if (!context2d) { return } context2d.clearRect(0, 0, this.target.width, this.target.height) if (this.isCacheFrame && this.frameCache[frame]) { const ofsFrame = this.frameCache[frame] context2d.drawImage(ofsFrame, 0, 0) return } const ofsCanvas = this.offscreenCanvas ofsCanvas.width = this.target.width ofsCanvas.height = this.target.height render(ofsCanvas, images, dynamicElements, sprites, frame) context2d.drawImage(ofsCanvas, 0, 0) if (this.isCacheFrame) { createImageBitmap(ofsCanvas).then((bitMap) => { this.frameCache[frame] = bitMap }) } } public pauseAllAudio(): void { this.audios.forEach(function (audio) { audio.pause() }) } public resumeAllAudio(): void { this.audios.forEach(function (audio) { audio.play() }) } public stopAllAudio(): void { this.audios.forEach(function (audio) { audio.pause() audio.currentTime = 0 }) } }