morethanwords
4 years ago
27 changed files with 845 additions and 1341 deletions
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
import { isInDOM, $rootScope, cancelEvent } from "../lib/utils"; |
||||
import appDownloadManager, { Progress } from "../lib/appManagers/appDownloadManager"; |
||||
|
||||
export default class ProgressivePreloader { |
||||
public preloader: HTMLDivElement; |
||||
private circle: SVGCircleElement; |
||||
|
||||
//private tempID = 0;
|
||||
private detached = true; |
||||
|
||||
private fileName: string; |
||||
public controller: AbortController; |
||||
|
||||
constructor(elem?: Element, private cancelable = true) { |
||||
this.preloader = document.createElement('div'); |
||||
this.preloader.classList.add('preloader-container'); |
||||
|
||||
this.preloader.innerHTML = ` |
||||
<div class="you-spin-me-round"> |
||||
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-circular" viewBox="25 25 50 50"> |
||||
<circle class="preloader-path-new" cx="50" cy="50" r="23" fill="none" stroke-miterlimit="10"/> |
||||
</svg> |
||||
</div>`;
|
||||
|
||||
if(cancelable) { |
||||
this.preloader.innerHTML += ` |
||||
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-close" viewBox="0 0 20 20"> |
||||
<line x1="0" y1="20" x2="20" y2="0" stroke-width="2" stroke-linecap="round"></line> |
||||
<line x1="0" y1="0" x2="20" y2="20" stroke-width="2" stroke-linecap="round"></line> |
||||
</svg>`;
|
||||
} else { |
||||
this.preloader.classList.add('preloader-swing'); |
||||
} |
||||
|
||||
this.circle = this.preloader.firstElementChild.firstElementChild.firstElementChild as SVGCircleElement; |
||||
|
||||
if(elem) { |
||||
this.attach(elem); |
||||
} |
||||
|
||||
if(this.cancelable) { |
||||
this.preloader.addEventListener('click', (e) => { |
||||
cancelEvent(e); |
||||
this.detach(); |
||||
|
||||
if(!this.fileName) { |
||||
return; |
||||
} |
||||
|
||||
const download = appDownloadManager.getDownload(this.fileName); |
||||
if(download && download.controller && !download.controller.signal.aborted) { |
||||
download.controller.abort(); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
downloadProgressHandler = (details: Progress) => { |
||||
if(details.done >= details.total) { |
||||
this.detach(); |
||||
} |
||||
|
||||
//console.log('preloader download', promise, details);
|
||||
let percents = details.done / details.total * 100; |
||||
this.setProgress(percents); |
||||
}; |
||||
|
||||
public attach(elem: Element, reset = true, fileName?: string, append = true) { |
||||
this.fileName = fileName; |
||||
if(this.fileName) { |
||||
const download = appDownloadManager.getDownload(fileName); |
||||
download.promise.catch(() => { |
||||
this.detach(); |
||||
}); |
||||
|
||||
appDownloadManager.addProgressCallback(this.fileName, this.downloadProgressHandler); |
||||
} |
||||
|
||||
this.detached = false; |
||||
window.requestAnimationFrame(() => { |
||||
if(this.detached) return; |
||||
this.detached = false; |
||||
|
||||
elem[append ? 'append' : 'prepend'](this.preloader); |
||||
|
||||
if(this.cancelable && reset) { |
||||
this.setProgress(0); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public detach() { |
||||
this.detached = true; |
||||
|
||||
if(this.preloader.parentElement) { |
||||
window.requestAnimationFrame(() => { |
||||
if(!this.detached) return; |
||||
this.detached = true; |
||||
|
||||
if(this.preloader.parentElement) { |
||||
this.preloader.parentElement.removeChild(this.preloader); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
public setProgress(percents: number) { |
||||
if(!isInDOM(this.circle)) { |
||||
return; |
||||
} |
||||
|
||||
if(percents == 0) { |
||||
this.circle.style.strokeDasharray = ''; |
||||
return; |
||||
} |
||||
|
||||
try { |
||||
let totalLength = this.circle.getTotalLength(); |
||||
//console.log('setProgress', (percents / 100 * totalLength));
|
||||
this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', 200'; |
||||
} catch(err) {} |
||||
} |
||||
} |
@ -1,326 +0,0 @@
@@ -1,326 +0,0 @@
|
||||
/* |
||||
* Copyright (c) 2018-present, Evgeny Nadymov |
||||
* |
||||
* This source code is licensed under the GPL v.3.0 license found in the |
||||
* LICENSE file in the root directory of this source tree. |
||||
*/ |
||||
|
||||
// @ts-ignore
|
||||
//import MP4Box from 'mp4box/dist/mp4box.all.min';
|
||||
import { logger, LogLevels } from './logger'; |
||||
|
||||
export default class MP4Source { |
||||
private mp4file: any; |
||||
private nextBufferStart = 0; |
||||
private mediaSource: MediaSource = null; |
||||
private ready = false; |
||||
private bufferedTime = 40; |
||||
|
||||
private beforeMoovBufferSize = 32 * 1024; |
||||
private moovBufferSize = 512 * 1024; |
||||
private bufferSize = 512 * 1024; |
||||
private seekBufferSize = 256 * 1024; |
||||
|
||||
private currentBufferSize = this.beforeMoovBufferSize; |
||||
private nbSamples = 10; |
||||
private expectedSize: number; |
||||
|
||||
private seeking = false; |
||||
private loading = false; |
||||
private url: string; |
||||
|
||||
private log = logger('MP4'/* , LogLevels.error */); |
||||
|
||||
//public onLoadBuffer: (offset: number)
|
||||
|
||||
constructor(private video: {duration: number, video: {expected_size: number}}, private getBufferAsync: (start: number, end: number) => Promise<ArrayBuffer>) { |
||||
this.expectedSize = this.video.video.expected_size; |
||||
|
||||
this.init(video.duration); |
||||
} |
||||
|
||||
init(videoDuration: number) { |
||||
const mediaSource = new MediaSource(); |
||||
mediaSource.addEventListener('sourceopen', () => { |
||||
this.log('[MediaSource] sourceopen start', this.mediaSource, this); |
||||
|
||||
if(this.mediaSource.sourceBuffers.length > 0) return; |
||||
|
||||
//const mp4File = MP4Box.createFile();
|
||||
const mp4File = (window as any).MP4Box.createFile(); |
||||
mp4File.onMoovStart = () => { |
||||
this.log('[MP4Box] onMoovStart'); |
||||
this.currentBufferSize = this.moovBufferSize; |
||||
}; |
||||
|
||||
mp4File.onError = (error: Error) => { |
||||
this.log('[MP4Box] onError', error); |
||||
}; |
||||
|
||||
mp4File.onReady = (info: any) => { |
||||
this.log('[MP4Box] onReady', info); |
||||
this.ready = true; |
||||
this.currentBufferSize = this.bufferSize; |
||||
const { isFragmented, timescale, fragment_duration, duration } = info; |
||||
|
||||
if(!fragment_duration && !duration) { |
||||
this.mediaSource.duration = videoDuration; |
||||
this.bufferedTime = videoDuration; |
||||
} else { |
||||
this.mediaSource.duration = isFragmented |
||||
? fragment_duration / timescale |
||||
: duration / timescale; |
||||
} |
||||
|
||||
this.initializeAllSourceBuffers(info); |
||||
}; |
||||
|
||||
mp4File.onSegment = (id: number, sb: any, buffer: ArrayBuffer, sampleNum: number, is_last: boolean) => { |
||||
const isLast = (sampleNum + this.nbSamples) > sb.nb_samples; |
||||
|
||||
this.log('[MP4Box] onSegment', id, buffer, `${sampleNum}/${sb.nb_samples}`, isLast, sb.timestampOffset, mediaSource, is_last); |
||||
|
||||
sb.segmentIndex++; |
||||
sb.pendingAppends.push({ id, buffer, sampleNum, is_last: isLast }); |
||||
|
||||
this.onUpdateEnd(sb, true, false); |
||||
}; |
||||
|
||||
this.mp4file = mp4File; |
||||
this.log('[MediaSource] sourceopen end', this, this.mp4file); |
||||
|
||||
this.loadNextBuffer(); |
||||
}); |
||||
|
||||
mediaSource.addEventListener('sourceended', () => { |
||||
this.log('[MediaSource] sourceended', mediaSource.readyState); |
||||
//this.getBufferAsync = null;
|
||||
}); |
||||
|
||||
mediaSource.addEventListener('sourceclose', () => { |
||||
this.log('[MediaSource] sourceclose', mediaSource.readyState); |
||||
//this.getBufferAsync = null;
|
||||
}); |
||||
|
||||
this.mediaSource = mediaSource; |
||||
} |
||||
|
||||
private onInitAppended(sb: any) { |
||||
sb.sampleNum = 0; |
||||
sb.addEventListener('updateend', () => this.onUpdateEnd(sb, true, true)); |
||||
/* In case there are already pending buffers we call onUpdateEnd to start appending them*/ |
||||
this.onUpdateEnd(sb, false, true); |
||||
|
||||
// @ts-ignore
|
||||
this.mediaSource.pendingInits--; |
||||
// @ts-ignore
|
||||
if(this.mediaSource.pendingInits === 0) { |
||||
this.log('onInitAppended start!'); |
||||
this.mp4file.start(); |
||||
|
||||
if(this.expectedSize > this.bufferSize) { |
||||
this.nextBufferStart = this.bufferSize; |
||||
} else { |
||||
return; |
||||
} |
||||
|
||||
/* setInterval(() => { |
||||
this.loadNextBuffer(); |
||||
}, 1e3); */ |
||||
this.loadNextBuffer(); |
||||
} |
||||
}; |
||||
|
||||
private onUpdateEnd(sb: any, isNotInit: boolean, isEndOfAppend: boolean) { |
||||
//console.this.log('onUpdateEnd', sb, isNotInit, isEndOfAppend, sb.sampleNum, sb.is_last);
|
||||
if(isEndOfAppend === true) { |
||||
if(sb.sampleNum) { |
||||
this.mp4file.releaseUsedSamples(sb.id, sb.sampleNum); |
||||
delete sb.sampleNum; |
||||
} |
||||
|
||||
if(sb.is_last) { |
||||
this.log('onUpdateEnd', sb, isNotInit, isEndOfAppend, sb.sampleNum, sb.is_last); |
||||
this.mediaSource.endOfStream(); |
||||
} |
||||
} |
||||
|
||||
if(this.mediaSource.readyState === "open" && sb.updating === false && sb.pendingAppends.length > 0) { |
||||
const obj = sb.pendingAppends.shift(); |
||||
this.log("MSE - SourceBuffer #"+sb.id, "Appending new buffer, pending: "+sb.pendingAppends.length); |
||||
sb.sampleNum = obj.sampleNum; |
||||
sb.is_last = obj.is_last; |
||||
sb.appendBuffer(obj.buffer); |
||||
} |
||||
} |
||||
|
||||
private initializeAllSourceBuffers(info: any) { |
||||
for(let i = 0; i < info.tracks.length; i++) { |
||||
this.addSourceBuffer(info.tracks[i]); |
||||
} |
||||
|
||||
this.initializeSourceBuffers(); |
||||
} |
||||
|
||||
private initializeSourceBuffers() { |
||||
const initSegs = this.mp4file.initializeSegmentation(); |
||||
this.log('[MP4Box] initializeSegmentation', initSegs); |
||||
|
||||
for(let i = 0; i < initSegs.length; i++) { |
||||
const sb: any = initSegs[i].user; |
||||
if(i === 0) { |
||||
// @ts-ignore
|
||||
this.mediaSource.pendingInits = 0; |
||||
} |
||||
|
||||
let onInitAppended = () => { |
||||
if(this.mediaSource.readyState === "open") { |
||||
sb.removeEventListener('updateend', onInitAppended); |
||||
this.onInitAppended(sb); |
||||
} |
||||
}; |
||||
|
||||
sb.addEventListener('updateend', onInitAppended); |
||||
sb.appendBuffer(initSegs[i].buffer); |
||||
sb.segmentIndex = 0; |
||||
|
||||
// @ts-ignore
|
||||
this.mediaSource.pendingInits++; |
||||
} |
||||
} |
||||
|
||||
private addSourceBuffer(track: {id: number, codec: string, type: 'video', nb_samples: number}) { |
||||
const file = this.mp4file; |
||||
const ms = this.mediaSource; |
||||
if(!track) return; |
||||
|
||||
const { id, codec, type: trackType, nb_samples } = track; |
||||
const mime = `video/mp4; codecs="${codec}"`; |
||||
this.log('mimetype:', mime); |
||||
if(!MediaSource.isTypeSupported(mime)) { |
||||
this.log('[addSourceBuffer] not supported', mime); |
||||
return; |
||||
} |
||||
|
||||
const sb: any = ms.addSourceBuffer(mime); |
||||
sb.id = id; |
||||
sb.pendingAppends = []; |
||||
sb.nb_samples = nb_samples; |
||||
file.setSegmentOptions(id, sb, { nbSamples: this.nbSamples }); |
||||
|
||||
this.log('[addSourceBuffer] add', id, codec, trackType, sb); |
||||
sb.addEventListener("error", (e: Event) => { |
||||
this.log("MSE SourceBuffer #" + id, e); |
||||
}); |
||||
} |
||||
|
||||
stop() { |
||||
this.mp4file.stop(); |
||||
this.mp4file = null; |
||||
this.getBufferAsync = null; |
||||
} |
||||
|
||||
getURL() { |
||||
return this.url ?? (this.url = URL.createObjectURL(this.mediaSource)); |
||||
} |
||||
|
||||
seek(currentTime: number/* , buffered: any */) { |
||||
const seekInfo: {offset: number, time: number} = this.mp4file.seek(currentTime, true); |
||||
this.nextBufferStart = seekInfo.offset; |
||||
|
||||
const loadNextBuffer = true; |
||||
/* let loadNextBuffer = buffered.length === 0; |
||||
for(let i = 0; i < buffered.length; i++) { |
||||
const start = buffered.start(i); |
||||
const end = buffered.end(i); |
||||
|
||||
if(start <= currentTime && currentTime + this.bufferedTime > end) { |
||||
loadNextBuffer = true; |
||||
break; |
||||
} |
||||
} */ |
||||
|
||||
this.log('[player] onSeeked', loadNextBuffer, currentTime, seekInfo, this.nextBufferStart); |
||||
if(loadNextBuffer) { |
||||
this.loadNextBuffer(true); |
||||
} |
||||
|
||||
return seekInfo.offset; |
||||
} |
||||
|
||||
timeUpdate(currentTime: number, duration: number, buffered: any) { |
||||
//return;
|
||||
|
||||
const ranges = []; |
||||
for(let i = 0; i < buffered.length; i++) { |
||||
ranges.push({ start: buffered.start(i), end: buffered.end(i)}) |
||||
} |
||||
|
||||
let loadNextBuffer = buffered.length === 0; |
||||
let hasRange = false; |
||||
for(let i = 0; i < buffered.length; i++) { |
||||
const start = buffered.start(i); |
||||
const end = buffered.end(i); |
||||
|
||||
if (start <= currentTime && currentTime <= end) { |
||||
hasRange = true; |
||||
if (end < duration && currentTime + this.bufferedTime > end) { |
||||
loadNextBuffer = true; |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if(!hasRange) { |
||||
loadNextBuffer = true; |
||||
} |
||||
|
||||
this.log('[player] timeUpdate', loadNextBuffer, currentTime, duration, JSON.stringify(ranges)); |
||||
if(loadNextBuffer) { |
||||
this.loadNextBuffer(); |
||||
} |
||||
} |
||||
|
||||
async loadNextBuffer(seek = false) { |
||||
const { nextBufferStart, loading, currentBufferSize, mp4file } = this; |
||||
this.log('[player] loadNextBuffer', nextBufferStart === undefined, loading, !mp4file); |
||||
if(!mp4file) return; |
||||
if(nextBufferStart === undefined) return; |
||||
if(loading) return; |
||||
|
||||
//return;
|
||||
|
||||
this.loading = true; |
||||
let bufferSize = seek ? this.seekBufferSize : this.bufferSize; |
||||
if(nextBufferStart + bufferSize > this.expectedSize) { |
||||
bufferSize = this.expectedSize - nextBufferStart; |
||||
} |
||||
const nextBuffer = await this.getBufferAsync(nextBufferStart, nextBufferStart + bufferSize); |
||||
// @ts-ignore
|
||||
nextBuffer.fileStart = nextBufferStart; |
||||
|
||||
const end = (nextBuffer.byteLength !== bufferSize)/* || (nextBuffer.byteLength === this.expectedSize) */; |
||||
|
||||
this.log('[player] loadNextBuffer start', nextBuffer.byteLength, nextBufferStart, end); |
||||
if(nextBuffer.byteLength) { |
||||
this.nextBufferStart = mp4file.appendBuffer(nextBuffer/* , end */); |
||||
} else { |
||||
this.nextBufferStart = undefined; |
||||
} |
||||
|
||||
if(end) { |
||||
this.log('[player] loadNextBuffer flush'); |
||||
this.mp4file.flush(); |
||||
} |
||||
|
||||
this.log('[player] loadNextBuffer stop', nextBuffer.byteLength, nextBufferStart, this.nextBufferStart); |
||||
|
||||
this.loading = false; |
||||
if(!this.ready || !end) { |
||||
this.log('[player] loadNextBuffer next'); |
||||
this.loadNextBuffer(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
(window as any).MP4Source = MP4Source; |
@ -0,0 +1,115 @@
@@ -0,0 +1,115 @@
|
||||
import { $rootScope } from "../utils"; |
||||
import apiManager from "../mtproto/mtprotoworker"; |
||||
|
||||
export type Download = {promise: Promise<Blob>, controller: AbortController}; |
||||
export type Progress = {done: number, fileName: string, total: number, offset: number}; |
||||
export type ProgressCallback = (details: Progress) => void; |
||||
|
||||
export class AppDownloadManager { |
||||
private downloads: {[fileName: string]: Download} = {}; |
||||
private progress: {[fileName: string]: Progress} = {}; |
||||
private progressCallbacks: {[fileName: string]: Array<ProgressCallback>} = {}; |
||||
|
||||
constructor() { |
||||
$rootScope.$on('download_progress', (e) => { |
||||
const details = e.detail as {done: number, fileName: string, total: number, offset: number}; |
||||
this.progress[details.fileName] = details; |
||||
|
||||
const callbacks = this.progressCallbacks[details.fileName]; |
||||
if(callbacks) { |
||||
callbacks.forEach(callback => callback(details)); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public download(fileName: string, url: string) { |
||||
if(this.downloads.hasOwnProperty(fileName)) return this.downloads[fileName]; |
||||
|
||||
const controller = new AbortController(); |
||||
const promise = fetch(url, {signal: controller.signal}) |
||||
.then(res => res.blob()) |
||||
.catch(err => { // Только потому что event.request.signal не работает в SW, либо я кривой?
|
||||
if(err.name === 'AbortError') { |
||||
//console.log('Fetch aborted');
|
||||
apiManager.cancelDownload(fileName); |
||||
delete this.downloads[fileName]; |
||||
delete this.progress[fileName]; |
||||
delete this.progressCallbacks[fileName]; |
||||
} else { |
||||
//console.error('Uh oh, an error!', err);
|
||||
} |
||||
|
||||
throw err; |
||||
}); |
||||
|
||||
//console.log('Will download file:', fileName, url);
|
||||
|
||||
promise.finally(() => { |
||||
delete this.progressCallbacks[fileName]; |
||||
}); |
||||
|
||||
return this.downloads[fileName] = {promise, controller}; |
||||
} |
||||
|
||||
public getDownload(fileName: string) { |
||||
return this.downloads[fileName]; |
||||
} |
||||
|
||||
public addProgressCallback(fileName: string, callback: ProgressCallback) { |
||||
const progress = this.progress[fileName]; |
||||
(this.progressCallbacks[fileName] ?? (this.progressCallbacks[fileName] = [])).push(callback); |
||||
|
||||
if(progress) { |
||||
callback(progress); |
||||
} |
||||
} |
||||
|
||||
private createDownloadAnchor(url: string, onRemove?: () => void) { |
||||
const a = document.createElement('a'); |
||||
a.href = url; |
||||
|
||||
a.style.position = 'absolute'; |
||||
a.style.top = '1px'; |
||||
a.style.left = '1px'; |
||||
|
||||
document.body.append(a); |
||||
|
||||
try { |
||||
var clickEvent = document.createEvent('MouseEvents'); |
||||
clickEvent.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); |
||||
a.dispatchEvent(clickEvent); |
||||
} catch (e) { |
||||
console.error('Download click error', e); |
||||
try { |
||||
a.click(); |
||||
} catch (e) { |
||||
window.open(url as string, '_blank'); |
||||
} |
||||
} |
||||
|
||||
setTimeout(() => { |
||||
a.remove(); |
||||
onRemove && onRemove(); |
||||
}, 100); |
||||
} |
||||
|
||||
/* public downloadToDisc(fileName: string, url: string) { |
||||
this.createDownloadAnchor(url); |
||||
|
||||
return this.download(fileName, url); |
||||
} */ |
||||
|
||||
public downloadToDisc(fileName: string, url: string) { |
||||
const download = this.download(fileName, url); |
||||
download.promise.then(blob => { |
||||
const objectURL = URL.createObjectURL(blob); |
||||
this.createDownloadAnchor(objectURL, () => { |
||||
URL.revokeObjectURL(objectURL); |
||||
}); |
||||
}); |
||||
|
||||
return download; |
||||
} |
||||
} |
||||
|
||||
export default new AppDownloadManager(); |
File diff suppressed because one or more lines are too long
@ -1,461 +0,0 @@
@@ -1,461 +0,0 @@
|
||||
import { isInDOM } from "./utils"; |
||||
|
||||
let convert = (value: number) => { |
||||
return Math.round(Math.min(Math.max(value, 0), 1) * 255); |
||||
}; |
||||
|
||||
type RLottiePlayerListeners = 'firstFrame' | 'enterFrame'; |
||||
|
||||
export class RLottiePlayer { |
||||
public static reqId = 0; |
||||
|
||||
public reqId = 0; |
||||
public curFrame: number; |
||||
public worker: QueryableWorker; |
||||
public el: HTMLElement; |
||||
public width: number; |
||||
public height: number; |
||||
|
||||
public listeners: Partial<{ |
||||
[k in RLottiePlayerListeners]: (res: any) => void |
||||
}> = {}; |
||||
public listenerResults: Partial<{ |
||||
[k in RLottiePlayerListeners]: any |
||||
}> = {}; |
||||
|
||||
public canvas: HTMLCanvasElement; |
||||
public context: CanvasRenderingContext2D; |
||||
|
||||
public paused = false; |
||||
public direction = 1; |
||||
public speed = 1; |
||||
public autoplay = true; |
||||
|
||||
constructor({el, width, height, worker}: { |
||||
el: HTMLElement, |
||||
width: number, |
||||
height: number, |
||||
worker: QueryableWorker |
||||
}) { |
||||
this.reqId = ++RLottiePlayer['reqId']; |
||||
this.el = el; |
||||
this.width = width; |
||||
this.height = height; |
||||
this.worker = worker; |
||||
} |
||||
|
||||
public addListener(name: RLottiePlayerListeners, callback: (res?: any) => void) { |
||||
if(this.listenerResults.hasOwnProperty(name)) return Promise.resolve(this.listenerResults[name]); |
||||
this.listeners[name] = callback; |
||||
} |
||||
|
||||
public setListenerResult(name: RLottiePlayerListeners, value?: any) { |
||||
this.listenerResults[name] = value; |
||||
if(this.listeners[name]) { |
||||
this.listeners[name](value); |
||||
} |
||||
} |
||||
|
||||
private sendQuery(methodName: string, ...args: any[]) { |
||||
this.worker.sendQuery(methodName, this.reqId, ...args); |
||||
} |
||||
|
||||
public loadFromData(json: any) { |
||||
this.sendQuery('loadFromData', json, this.width, this.height, { |
||||
paused: this.paused, |
||||
direction: this.direction, |
||||
speed: this.speed |
||||
}); |
||||
} |
||||
|
||||
public play() { |
||||
this.sendQuery('play'); |
||||
this.paused = false; |
||||
} |
||||
|
||||
public pause() { |
||||
this.sendQuery('pause'); |
||||
this.paused = true; |
||||
} |
||||
|
||||
public stop() { |
||||
this.sendQuery('stop'); |
||||
this.paused = true; |
||||
} |
||||
|
||||
public restart() { |
||||
this.sendQuery('restart'); |
||||
} |
||||
|
||||
public setSpeed(speed: number) { |
||||
this.sendQuery('setSpeed', speed); |
||||
} |
||||
|
||||
public setDirection(direction: number) { |
||||
this.direction = direction; |
||||
this.sendQuery('setDirection', direction); |
||||
} |
||||
|
||||
public destroy() { |
||||
lottieLoader.onDestroy(this.reqId); |
||||
this.sendQuery('destroy'); |
||||
} |
||||
|
||||
private attachPlayer() { |
||||
this.canvas = document.createElement('canvas'); |
||||
this.canvas.width = this.width; |
||||
this.canvas.height = this.height; |
||||
|
||||
//this.el.appendChild(this.canvas);
|
||||
this.context = this.canvas.getContext('2d'); |
||||
} |
||||
|
||||
public renderFrame(frame: Uint8ClampedArray, frameNo: number) { |
||||
if(!this.listenerResults.hasOwnProperty('firstFrame')) { |
||||
this.attachPlayer(); |
||||
this.el.appendChild(this.canvas); |
||||
|
||||
this.setListenerResult('firstFrame'); |
||||
} |
||||
|
||||
this.context.putImageData(new ImageData(frame, this.width, this.height), 0, 0); |
||||
this.setListenerResult('enterFrame', frameNo); |
||||
} |
||||
} |
||||
|
||||
class QueryableWorker { |
||||
private worker: Worker; |
||||
private listeners: {[name: string]: (...args: any[]) => void} = {}; |
||||
|
||||
constructor(url: string, private defaultListener: (data: any) => void = () => {}, onError?: (error: any) => void) { |
||||
this.worker = new Worker(url); |
||||
if(onError) { |
||||
this.worker.onerror = onError; |
||||
} |
||||
|
||||
this.worker.onmessage = (event) => { |
||||
if(event.data instanceof Object && |
||||
event.data.hasOwnProperty('queryMethodListener') && |
||||
event.data.hasOwnProperty('queryMethodArguments')) { |
||||
this.listeners[event.data.queryMethodListener].apply(this, event.data.queryMethodArguments); |
||||
} else { |
||||
this.defaultListener.call(this, event.data); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public postMessage(message: any) { |
||||
this.worker.postMessage(message); |
||||
} |
||||
|
||||
public terminate() { |
||||
this.worker.terminate(); |
||||
} |
||||
|
||||
public addListener(name: string, listener: (...args: any[]) => void) { |
||||
this.listeners[name] = listener; |
||||
} |
||||
|
||||
public removeListener(name: string) { |
||||
delete this.listeners[name]; |
||||
} |
||||
|
||||
public sendQuery(queryMethod: string, ...args: any[]) { |
||||
this.worker.postMessage({ |
||||
'queryMethod': queryMethod, |
||||
'queryMethodArguments': args |
||||
}); |
||||
} |
||||
} |
||||
|
||||
class LottieLoader { |
||||
public loadPromise: Promise<void>; |
||||
public loaded = false; |
||||
|
||||
private static COLORREPLACEMENTS = [ |
||||
[ |
||||
[0xf77e41, 0xca907a], |
||||
[0xffb139, 0xedc5a5], |
||||
[0xffd140, 0xf7e3c3], |
||||
[0xffdf79, 0xfbefd6], |
||||
], |
||||
|
||||
[ |
||||
[0xf77e41, 0xaa7c60], |
||||
[0xffb139, 0xc8a987], |
||||
[0xffd140, 0xddc89f], |
||||
[0xffdf79, 0xe6d6b2], |
||||
], |
||||
|
||||
[ |
||||
[0xf77e41, 0x8c6148], |
||||
[0xffb139, 0xad8562], |
||||
[0xffd140, 0xc49e76], |
||||
[0xffdf79, 0xd4b188], |
||||
], |
||||
|
||||
[ |
||||
[0xf77e41, 0x6e3c2c], |
||||
[0xffb139, 0x925a34], |
||||
[0xffd140, 0xa16e46], |
||||
[0xffdf79, 0xac7a52], |
||||
] |
||||
]; |
||||
|
||||
private workersLimit = 4; |
||||
private players: {[reqId: number]: RLottiePlayer} = {}; |
||||
private byGroups: {[group: string]: RLottiePlayer[]} = {}; |
||||
|
||||
private workers: QueryableWorker[] = []; |
||||
private curWorkerNum = 0; |
||||
|
||||
private observer: IntersectionObserver; |
||||
private visible: Set<RLottiePlayer> = new Set(); |
||||
|
||||
private debug = true; |
||||
|
||||
constructor() { |
||||
this.observer = new IntersectionObserver((entries) => { |
||||
for(const entry of entries) { |
||||
const target = entry.target; |
||||
|
||||
for(const group in this.byGroups) { |
||||
const player = this.byGroups[group].find(p => p.el == target); |
||||
if(player) { |
||||
if(entry.isIntersecting) { |
||||
this.visible.add(player); |
||||
|
||||
if(player.paused) { |
||||
player.play(); |
||||
} |
||||
} else { |
||||
this.visible.delete(player); |
||||
|
||||
if(!player.paused) { |
||||
player.pause(); |
||||
} |
||||
} |
||||
|
||||
break; |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
public loadLottieWorkers() { |
||||
if(this.loadPromise) return this.loadPromise; |
||||
|
||||
const onFrame = this.onFrame.bind(this); |
||||
|
||||
return this.loadPromise = new Promise((resolve, reject) => { |
||||
let remain = this.workersLimit; |
||||
for(let i = 0; i < this.workersLimit; ++i) { |
||||
const worker = this.workers[i] = new QueryableWorker('rlottie.worker.js'); |
||||
|
||||
worker.addListener('ready', () => { |
||||
console.log('worker #' + i + ' ready'); |
||||
|
||||
worker.addListener('frame', onFrame); |
||||
|
||||
--remain; |
||||
if(!remain) { |
||||
console.log('workers ready'); |
||||
resolve(); |
||||
this.loaded = true; |
||||
} |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
|
||||
private applyReplacements(object: any, toneIndex: number) { |
||||
const replacements = LottieLoader.COLORREPLACEMENTS[toneIndex - 2]; |
||||
|
||||
const iterateIt = (it: any) => { |
||||
for(let smth of it) { |
||||
switch(smth.ty) { |
||||
case 'st': |
||||
case 'fl': |
||||
let k = smth.c.k; |
||||
let color = convert(k[2]) | (convert(k[1]) << 8) | (convert(k[0]) << 16); |
||||
|
||||
let foundReplacement = replacements.find(p => p[0] == color); |
||||
if(foundReplacement) { |
||||
k[0] = ((foundReplacement[1] >> 16) & 255) / 255; |
||||
k[1] = ((foundReplacement[1] >> 8) & 255) / 255; |
||||
k[2] = (foundReplacement[1] & 255) / 255; |
||||
} |
||||
|
||||
//console.log('foundReplacement!', foundReplacement, color.toString(16), k);
|
||||
break; |
||||
} |
||||
|
||||
if(smth.hasOwnProperty('it')) { |
||||
iterateIt(smth.it); |
||||
} |
||||
} |
||||
}; |
||||
|
||||
for(let layer of object.layers) { |
||||
if(!layer.shapes) continue; |
||||
|
||||
for(let shape of layer.shapes) { |
||||
iterateIt(shape.it); |
||||
} |
||||
} |
||||
} |
||||
|
||||
public async loadAnimationWorker(params: { |
||||
container: HTMLElement, |
||||
autoplay?: boolean, |
||||
animationData: any, |
||||
loop?: boolean, |
||||
renderer?: string, |
||||
width?: number, |
||||
height?: number |
||||
}, group = '', toneIndex = -1) { |
||||
//params.autoplay = false;
|
||||
|
||||
if(toneIndex >= 1 && toneIndex <= 5) { |
||||
this.applyReplacements(params.animationData, toneIndex); |
||||
} |
||||
|
||||
if(!this.loaded) { |
||||
await this.loadLottieWorkers(); |
||||
} |
||||
|
||||
this.observer.observe(params.container); |
||||
|
||||
const width = params.width || parseInt(params.container.style.width); |
||||
const height = params.height || parseInt(params.container.style.height); |
||||
|
||||
const player = this.initPlayer(params.container, params.animationData, width, height); |
||||
for(let i in params) { |
||||
// @ts-ignore
|
||||
if(player.hasOwnProperty(i)) { |
||||
// @ts-ignore
|
||||
player[i] = params[i]; |
||||
} |
||||
} |
||||
|
||||
(this.byGroups[group] ?? (this.byGroups[group] = [])).push(player); |
||||
|
||||
return player; |
||||
} |
||||
|
||||
public checkAnimations(blurred?: boolean, group?: string, destroy = false) { |
||||
const groups = group && false ? [group] : Object.keys(this.byGroups); |
||||
|
||||
if(group && !this.byGroups[group]) { |
||||
console.warn('no animation group:', group); |
||||
this.byGroups[group] = []; |
||||
//return;
|
||||
} |
||||
|
||||
for(const group of groups) { |
||||
const animations = this.byGroups[group]; |
||||
|
||||
const length = animations.length; |
||||
for(let i = length - 1; i >= 0; --i) { |
||||
const player = animations[i]; |
||||
|
||||
if(destroy || (!isInDOM(player.el) && player.listenerResults.hasOwnProperty('firstFrame'))) { |
||||
//console.log('destroy animation');
|
||||
player.destroy(); |
||||
continue; |
||||
} |
||||
|
||||
if(blurred) { |
||||
if(!player.paused) { |
||||
this.debug && console.log('pause animation', player); |
||||
player.pause(); |
||||
} |
||||
} else if(player.paused && this.visible.has(player)) { |
||||
this.debug && console.log('play animation', player); |
||||
player.play(); |
||||
} |
||||
|
||||
/* if(canvas) { |
||||
let c = container.firstElementChild as HTMLCanvasElement; |
||||
if(!c) { |
||||
console.warn('no canvas element for check!', container, animations[i]); |
||||
continue; |
||||
} |
||||
|
||||
if(!c.height && !c.width && isElementInViewport(container)) { |
||||
//console.log('lottie need resize');
|
||||
animation.resize(); |
||||
} |
||||
} */ |
||||
|
||||
//if(!autoplay) continue;
|
||||
|
||||
/* if(blurred || !isElementInViewport(container)) { |
||||
if(!paused) { |
||||
this.debug && console.log('pause animation', isElementInViewport(container), container); |
||||
animation.pause(); |
||||
animations[i].paused = true; |
||||
} |
||||
} else if(paused) { |
||||
this.debug && console.log('play animation', container); |
||||
animation.play(); |
||||
animations[i].paused = false; |
||||
} */ |
||||
} |
||||
} |
||||
} |
||||
|
||||
private onFrame(reqId: number, frameNo: number, frame: Uint8ClampedArray, width: number, height: number) { |
||||
const rlPlayer = this.players[reqId]; |
||||
if(!rlPlayer) { |
||||
this.debug && console.warn('onFrame on destroyed player:', reqId, frameNo); |
||||
return; |
||||
} |
||||
|
||||
rlPlayer.renderFrame(frame, frameNo); |
||||
} |
||||
|
||||
public onDestroy(reqId: number) { |
||||
let player = this.players[reqId]; |
||||
for(let group in this.byGroups) { |
||||
this.byGroups[group].findAndSplice(p => p == player); |
||||
} |
||||
|
||||
delete this.players[player.reqId]; |
||||
this.observer.unobserve(player.el); |
||||
this.visible.delete(player); |
||||
} |
||||
|
||||
public destroyWorkers() { |
||||
this.workers.forEach((worker, idx) => { |
||||
worker.terminate(); |
||||
console.log('worker #' + idx + ' terminated'); |
||||
}); |
||||
|
||||
console.log('workers destroyed'); |
||||
this.workers.length = 0; |
||||
} |
||||
|
||||
private initPlayer(el: HTMLElement, json: any, width: number, height: number) { |
||||
const rlPlayer = new RLottiePlayer({ |
||||
el, |
||||
width, |
||||
height, |
||||
worker: this.workers[this.curWorkerNum++] |
||||
}); |
||||
|
||||
this.players[rlPlayer.reqId] = rlPlayer; |
||||
if(this.curWorkerNum >= this.workers.length) { |
||||
this.curWorkerNum = 0; |
||||
} |
||||
|
||||
rlPlayer.loadFromData(json); |
||||
|
||||
return rlPlayer; |
||||
} |
||||
} |
||||
|
||||
const lottieLoader = new LottieLoader(); |
||||
(window as any).LottieLoader = lottieLoader; |
||||
export default lottieLoader; |
@ -1,24 +0,0 @@
@@ -1,24 +0,0 @@
|
||||
import insideWorker from 'offscreen-canvas/inside-worker'; |
||||
|
||||
console.log(self); |
||||
|
||||
import { Webp } from "./libwebp.js"; |
||||
let webp = new Webp(); |
||||
webp.Module.doNotCaptureKeyboard = true; |
||||
webp.Module.noImageDecoding = true; |
||||
|
||||
let canvas = null; |
||||
|
||||
const worker = insideWorker(e => { |
||||
if(e.data.canvas) { |
||||
canvas = e.data.canvas; |
||||
console.log(e, canvas); |
||||
webp.setCanvas(canvas); |
||||
//webp.webpToSdl()
|
||||
// Draw on the canvas
|
||||
} else if(e.data.message == 'webpBytes') { |
||||
webp.webpToSdl(e.data.bytes, e.data.bytes.length); |
||||
//console.log(canvas);
|
||||
self.postMessage({converted: true}); |
||||
} |
||||
}); |
Loading…
Reference in new issue