morethanwords
4 years ago
27 changed files with 845 additions and 1341 deletions
@ -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 @@ |
|||||||
/* |
|
||||||
* 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 @@ |
|||||||
|
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 @@ |
|||||||
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 @@ |
|||||||
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