SW:
Downloads; Audio & video streaming; Download controller
This commit is contained in:
parent
23c91473f6
commit
617acdbb13
9
package-lock.json
generated
9
package-lock.json
generated
@ -9688,15 +9688,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mp4box": {
|
||||
"version": "0.3.20",
|
||||
"resolved": "https://registry.npmjs.org/mp4box/-/mp4box-0.3.20.tgz",
|
||||
"integrity": "sha512-9I1wOBql0c9BsIPDGHY97dcH5kT7hG0Tx6SAaJvXf+A6Z0zBfGy7L1vEfjMKgjXSjtdXWL7gO+8a5euikaFTEA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"npm": "^6.9.0"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
|
@ -50,7 +50,6 @@
|
||||
"lottie-web": "^5.6.10",
|
||||
"media-query-plugin": "^1.3.1",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"mp4box": "^0.3.20",
|
||||
"node-sass": "^4.14.1",
|
||||
"npm": "^6.14.5",
|
||||
"on-build-webpack": "^0.1.0",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { isInDOM } from "../lib/utils";
|
||||
import { isInDOM, $rootScope } from "../lib/utils";
|
||||
import { RLottiePlayer } from "../lib/lottieLoader";
|
||||
|
||||
export interface AnimationItem {
|
||||
@ -18,6 +18,8 @@ export class AnimationIntersector {
|
||||
|
||||
constructor() {
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
if($rootScope.idle.isIDLE) return;
|
||||
|
||||
for(const entry of entries) {
|
||||
const target = entry.target;
|
||||
|
||||
@ -72,6 +74,8 @@ export class AnimationIntersector {
|
||||
}
|
||||
|
||||
public checkAnimations(blurred?: boolean, group?: string, destroy = false) {
|
||||
if($rootScope.idle.isIDLE) return;
|
||||
|
||||
const groups = group /* && false */ ? [group] : Object.keys(this.byGroups);
|
||||
|
||||
if(group && !this.byGroups[group]) {
|
||||
|
@ -49,10 +49,7 @@ class AppAudio {
|
||||
audio.addEventListener('pause', this.onPause);
|
||||
audio.addEventListener('ended', this.onEnded);
|
||||
|
||||
appDocsManager.downloadDoc(doc.id).then(() => {
|
||||
this.container.append(audio);
|
||||
source.src = doc.url;
|
||||
}, () => {
|
||||
const onError = (e: Event) => {
|
||||
if(this.nextMid == mid) {
|
||||
this.loadSiblingsAudio(doc.type as 'voice' | 'audio', mid).then(() => {
|
||||
if(this.nextMid && this.audios[this.nextMid]) {
|
||||
@ -60,7 +57,16 @@ class AppAudio {
|
||||
}
|
||||
})
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
audio.addEventListener('error', onError);
|
||||
|
||||
const downloadPromise: Promise<any> = !doc.supportsStreaming ? appDocsManager.downloadDocNew(doc.id).promise : Promise.resolve();
|
||||
|
||||
downloadPromise.then(() => {
|
||||
this.container.append(audio);
|
||||
source.src = doc.url;
|
||||
}, onError);
|
||||
|
||||
return this.audios[mid] = audio;
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
import appDocsManager from "../lib/appManagers/appDocsManager";
|
||||
import { RichTextProcessor } from "../lib/richtextprocessor";
|
||||
import { formatDate } from "./wrappers";
|
||||
import ProgressivePreloader from "./preloader";
|
||||
import { CancellablePromise } from "../lib/polyfill";
|
||||
import ProgressivePreloader from "./preloader_new";
|
||||
import { MediaProgressLine } from "../lib/mediaPlayer";
|
||||
import appAudio from "./appAudio";
|
||||
import { MTDocument } from "../types";
|
||||
import { mediaSizes } from "../lib/config";
|
||||
import { Download } from "../lib/appManagers/appDownloadManager";
|
||||
|
||||
// https://github.com/LonamiWebs/Telethon/blob/4393ec0b83d511b6a20d8a20334138730f084375/telethon/utils.py#L1285
|
||||
export function decodeWaveform(waveform: Uint8Array | number[]) {
|
||||
@ -233,7 +233,7 @@ function wrapAudio(doc: MTDocument, audioEl: AudioElement) {
|
||||
const subtitleDiv = audioEl.querySelector('.audio-subtitle') as HTMLDivElement;
|
||||
let launched = false;
|
||||
|
||||
let progressLine = new MediaProgressLine(audioEl.audio);
|
||||
let progressLine = new MediaProgressLine(audioEl.audio, doc.supportsStreaming);
|
||||
|
||||
audioEl.addAudioListener('ended', () => {
|
||||
audioEl.classList.remove('audio-show-progress');
|
||||
@ -295,19 +295,23 @@ export default class AudioElement extends HTMLElement {
|
||||
|
||||
const durationStr = String(doc.duration | 0).toHHMMSS(true);
|
||||
|
||||
this.innerHTML = `
|
||||
<div class="audio-toggle audio-ico tgico-largeplay"></div>
|
||||
<div class="audio-download">${uploading ? '' : '<div class="tgico-download"></div>'}</div>`;
|
||||
this.innerHTML = `<div class="audio-toggle audio-ico tgico-largeplay"></div>`;
|
||||
|
||||
const downloadDiv = document.createElement('div');
|
||||
downloadDiv.classList.add('audio-download');
|
||||
if(!uploading && doc.type != 'audio') {
|
||||
downloadDiv.innerHTML = '<div class="tgico-download"></div>';
|
||||
}
|
||||
|
||||
if(doc.type != 'audio' && !uploading) {
|
||||
this.append(downloadDiv);
|
||||
}
|
||||
|
||||
const onTypeLoad = doc.type == 'voice' ? wrapVoiceMessage(doc, this) : wrapAudio(doc, this);
|
||||
|
||||
const downloadDiv = this.querySelector('.audio-download') as HTMLDivElement;
|
||||
const audioTimeDiv = this.querySelector('.audio-time') as HTMLDivElement;
|
||||
audioTimeDiv.innerHTML = durationStr;
|
||||
|
||||
let preloader: ProgressivePreloader = this.preloader;
|
||||
let promise: CancellablePromise<Blob>;
|
||||
|
||||
const onLoad = () => {
|
||||
const audio = this.audio = appAudio.addAudio(doc, mid);
|
||||
|
||||
@ -351,35 +355,64 @@ export default class AudioElement extends HTMLElement {
|
||||
};
|
||||
|
||||
if(!uploading) {
|
||||
const onClick = () => {
|
||||
if(!promise) {
|
||||
let preloader: ProgressivePreloader = this.preloader;
|
||||
|
||||
if(doc.type == 'voice') {
|
||||
let download: Download;
|
||||
|
||||
const onClick = () => {
|
||||
if(!download) {
|
||||
if(!preloader) {
|
||||
preloader = new ProgressivePreloader(null, true);
|
||||
}
|
||||
|
||||
download = appDocsManager.downloadDocNew(doc.id);
|
||||
preloader.attach(downloadDiv, true, appDocsManager.getInputFileName(doc));
|
||||
|
||||
download.promise.then(() => {
|
||||
downloadDiv.remove();
|
||||
this.removeEventListener('click', onClick);
|
||||
onLoad();
|
||||
}).catch(err => {
|
||||
if(err.name === 'AbortError') {
|
||||
download = null;
|
||||
}
|
||||
}).finally(() => {
|
||||
downloadDiv.classList.remove('downloading');
|
||||
});
|
||||
|
||||
downloadDiv.classList.add('downloading');
|
||||
} else {
|
||||
download.controller.abort();
|
||||
}
|
||||
};
|
||||
|
||||
this.addEventListener('click', onClick);
|
||||
this.click();
|
||||
} else {
|
||||
const r = () => {
|
||||
onLoad();
|
||||
|
||||
if(!preloader) {
|
||||
preloader = new ProgressivePreloader(null, true);
|
||||
preloader = new ProgressivePreloader(null, false);
|
||||
}
|
||||
|
||||
promise = appDocsManager.downloadDoc(doc.id);
|
||||
preloader.attach(downloadDiv, true, promise);
|
||||
preloader.attach(downloadDiv);
|
||||
this.append(downloadDiv);
|
||||
|
||||
promise.then(() => {
|
||||
preloader = null;
|
||||
downloadDiv.classList.remove('downloading');
|
||||
new Promise((resolve) => {
|
||||
if(this.audio.readyState >= 2) resolve();
|
||||
else this.addAudioListener('canplay', resolve);
|
||||
}).then(() => {
|
||||
downloadDiv.remove();
|
||||
this.removeEventListener('click', onClick);
|
||||
onLoad();
|
||||
this.audio.play();
|
||||
});
|
||||
};
|
||||
|
||||
downloadDiv.classList.add('downloading');
|
||||
} else {
|
||||
downloadDiv.classList.remove('downloading');
|
||||
promise.cancel();
|
||||
promise = null;
|
||||
}
|
||||
};
|
||||
|
||||
this.addEventListener('click', onClick);
|
||||
this.click();
|
||||
this.addEventListener('click', r, {once: true});
|
||||
}
|
||||
} else {
|
||||
this.preloader.attach(this.querySelector('.audio-download'), false);
|
||||
this.preloader.attach(downloadDiv, false);
|
||||
//onLoad();
|
||||
}
|
||||
}
|
||||
|
123
src/components/preloader_new.ts
Normal file
123
src/components/preloader_new.ts
Normal file
@ -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) {}
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import LottieLoader from '../lib/lottieLoader';
|
||||
import appDocsManager from "../lib/appManagers/appDocsManager";
|
||||
import { formatBytes, getEmojiToneIndex } from "../lib/utils";
|
||||
import ProgressivePreloader from './preloader';
|
||||
import ProgressivePreloaderNew from './preloader_new';
|
||||
import LazyLoadQueue from './lazyLoadQueue';
|
||||
import VideoPlayer from '../lib/mediaPlayer';
|
||||
import { RichTextProcessor } from '../lib/richtextprocessor';
|
||||
@ -18,6 +19,7 @@ import { mediaSizes } from '../lib/config';
|
||||
import { MTDocument, MTPhotoSize } from '../types';
|
||||
import animationIntersector from './animationIntersector';
|
||||
import AudioElement from './audio';
|
||||
import { Download } from '../lib/appManagers/appDownloadManager';
|
||||
|
||||
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue}: {
|
||||
doc: MTDocument,
|
||||
@ -93,14 +95,14 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
|
||||
if(message.media.preloader) { // means upload
|
||||
(message.media.preloader as ProgressivePreloader).attach(container, undefined, undefined, false);
|
||||
} else if(!doc.downloaded) {
|
||||
const promise = appDocsManager.downloadDoc(doc.id);
|
||||
/* const promise = appDocsManager.downloadDoc(doc.id);
|
||||
|
||||
//if(!doc.supportsStreaming) {
|
||||
const preloader = new ProgressivePreloader(container, true);
|
||||
preloader.attach(container, true, promise, false);
|
||||
//}
|
||||
|
||||
await promise;
|
||||
await promise; */
|
||||
}
|
||||
|
||||
if(middleware && !middleware()) {
|
||||
@ -215,34 +217,35 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals
|
||||
|
||||
if(!uploading) {
|
||||
let downloadDiv = docDiv.querySelector('.document-download') as HTMLDivElement;
|
||||
let preloader: ProgressivePreloader;
|
||||
let promise: CancellablePromise<Blob>;
|
||||
let preloader: ProgressivePreloaderNew;
|
||||
let download: Download;
|
||||
|
||||
docDiv.addEventListener('click', () => {
|
||||
if(!promise) {
|
||||
if(!download) {
|
||||
if(downloadDiv.classList.contains('downloading')) {
|
||||
return; // means not ready yet
|
||||
}
|
||||
|
||||
if(!preloader) {
|
||||
preloader = new ProgressivePreloader(null, true);
|
||||
preloader = new ProgressivePreloaderNew(null, true);
|
||||
}
|
||||
|
||||
appDocsManager.saveDocFile(doc.id).then(res => {
|
||||
promise = res.promise;
|
||||
download = appDocsManager.saveDocFile(doc);
|
||||
preloader.attach(downloadDiv, true, appDocsManager.getInputFileName(doc));
|
||||
|
||||
preloader.attach(downloadDiv, true, promise);
|
||||
|
||||
promise.then(() => {
|
||||
downloadDiv.classList.remove('downloading');
|
||||
downloadDiv.remove();
|
||||
});
|
||||
})
|
||||
download.promise.then(() => {
|
||||
downloadDiv.remove();
|
||||
}).catch(err => {
|
||||
if(err.name === 'AbortError') {
|
||||
download = null;
|
||||
}
|
||||
}).finally(() => {
|
||||
downloadDiv.classList.remove('downloading');
|
||||
});
|
||||
|
||||
downloadDiv.classList.add('downloading');
|
||||
} else {
|
||||
downloadDiv.classList.remove('downloading');
|
||||
promise = null;
|
||||
download.controller.abort();
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -480,7 +483,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
|
||||
}
|
||||
|
||||
let downloaded = doc.downloaded;
|
||||
let load = () => appDocsManager.downloadDoc(doc.id).then(blob => {
|
||||
let load = () => appDocsManager.downloadDocNew(doc.id).promise.then(blob => {
|
||||
//console.log('loaded sticker:', doc, div);
|
||||
if(middleware && !middleware()) return;
|
||||
|
||||
|
@ -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;
|
@ -924,7 +924,7 @@ export class AppDialogsManager {
|
||||
dom = this.getDialogDom(dialog.peerID);
|
||||
|
||||
if(!dom) {
|
||||
this.log.error('no dom for dialog:', dialog, lastMessage, dom, highlightWord);
|
||||
//this.log.error('no dom for dialog:', dialog, lastMessage, dom, highlightWord);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@ -1034,7 +1034,7 @@ export class AppDialogsManager {
|
||||
}
|
||||
|
||||
if(!dom) {
|
||||
this.log.error('setUnreadMessages no dom!', dialog);
|
||||
//this.log.error('setUnreadMessages no dom!', dialog);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
import FileManager from '../filemanager';
|
||||
import {RichTextProcessor} from '../richtextprocessor';
|
||||
import { CancellablePromise, deferredPromise } from '../polyfill';
|
||||
import { isObject, getFileURL } from '../utils';
|
||||
import opusDecodeController from '../opusDecodeController';
|
||||
import { MTDocument, inputDocumentFileLocation } from '../../types';
|
||||
import { getFileNameByLocation } from '../bin_utils';
|
||||
import appDownloadManager, { Download } from './appDownloadManager';
|
||||
|
||||
class AppDocsManager {
|
||||
private docs: {[docID: string]: MTDocument} = {};
|
||||
@ -47,13 +48,17 @@ class AppDocsManager {
|
||||
apiDoc.audioTitle = attribute.title;
|
||||
apiDoc.audioPerformer = attribute.performer;
|
||||
apiDoc.type = attribute.pFlags.voice && apiDoc.mime_type == "audio/ogg" ? 'voice' : 'audio';
|
||||
|
||||
if(apiDoc.type == 'audio') {
|
||||
apiDoc.supportsStreaming = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'documentAttributeVideo':
|
||||
apiDoc.duration = attribute.duration;
|
||||
apiDoc.w = attribute.w;
|
||||
apiDoc.h = attribute.h;
|
||||
apiDoc.supportsStreaming = attribute.pFlags?.supports_streaming && apiDoc.size > 524288 && typeof(MediaSource) !== 'undefined';
|
||||
apiDoc.supportsStreaming = attribute.pFlags?.supports_streaming/* && apiDoc.size > 524288 */;
|
||||
if(apiDoc.thumbs && attribute.pFlags.round_message) {
|
||||
apiDoc.type = 'round';
|
||||
} else /* if(apiDoc.thumbs) */ {
|
||||
@ -134,6 +139,10 @@ class AppDocsManager {
|
||||
apiDoc.size = 0;
|
||||
}
|
||||
|
||||
if(!apiDoc.url) {
|
||||
apiDoc.url = this.getFileURLByDoc(apiDoc);
|
||||
}
|
||||
|
||||
return apiDoc;
|
||||
}
|
||||
|
||||
@ -181,9 +190,21 @@ class AppDocsManager {
|
||||
return 't_' + (doc.type || 'file') + doc.id + fileExt;
|
||||
}
|
||||
|
||||
public getFileURLByDoc(doc: MTDocument) {
|
||||
public getFileURLByDoc(doc: MTDocument, download = false) {
|
||||
const inputFileLocation = this.getInputByID(doc);
|
||||
return getFileURL('document', {dcID: doc.dc_id, location: inputFileLocation, size: doc.size, mimeType: doc.mime_type || 'application/octet-stream'});
|
||||
const type = download ? 'download' : (doc.supportsStreaming ? 'stream' : 'document');
|
||||
|
||||
return getFileURL(type, {
|
||||
dcID: doc.dc_id,
|
||||
location: inputFileLocation,
|
||||
size: doc.size,
|
||||
mimeType: doc.mime_type || 'application/octet-stream',
|
||||
fileName: doc.file_name
|
||||
});
|
||||
}
|
||||
|
||||
public getInputFileName(doc: MTDocument) {
|
||||
return getFileNameByLocation(this.getInputByID(doc));
|
||||
}
|
||||
|
||||
public downloadDoc(docID: string | MTDocument, toFileEntry?: any): CancellablePromise<Blob> {
|
||||
@ -252,6 +273,56 @@ class AppDocsManager {
|
||||
return this.downloadPromises[doc.id] = deferred;
|
||||
}
|
||||
|
||||
public downloadDocNew(docID: string | MTDocument, toFileEntry?: any): Download {
|
||||
const doc = this.getDoc(docID);
|
||||
|
||||
if(doc._ == 'documentEmpty') {
|
||||
throw new Error('Document empty!');
|
||||
}
|
||||
|
||||
const fileName = this.getInputFileName(doc);
|
||||
|
||||
let download = appDownloadManager.getDownload(fileName);
|
||||
if(download) {
|
||||
return download;
|
||||
}
|
||||
|
||||
download = appDownloadManager.download(fileName, doc.url);
|
||||
//const _download: Download = {...download};
|
||||
|
||||
//_download.promise = _download.promise.then(async(blob) => {
|
||||
download.promise = download.promise.then(async(blob) => {
|
||||
if(blob) {
|
||||
doc.downloaded = true;
|
||||
|
||||
if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()) {
|
||||
let reader = new FileReader();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
reader.onloadend = (e) => {
|
||||
let uint8 = new Uint8Array(e.target.result as ArrayBuffer);
|
||||
//console.log('sending uint8 to decoder:', uint8);
|
||||
opusDecodeController.decode(uint8).then(result => {
|
||||
doc.url = result.url;
|
||||
resolve();
|
||||
}, (err) => {
|
||||
delete doc.downloaded;
|
||||
reject(err);
|
||||
});
|
||||
};
|
||||
|
||||
reader.readAsArrayBuffer(blob);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return blob;
|
||||
});
|
||||
|
||||
//return this.downloadPromisesNew[doc.id] = _download;
|
||||
return download;
|
||||
}
|
||||
|
||||
public downloadDocThumb(docID: any, thumbSize: string) {
|
||||
let doc = this.getDoc(docID);
|
||||
|
||||
@ -274,32 +345,11 @@ class AppDocsManager {
|
||||
return !!this.thumbs[docID + '-' + thumbSize];
|
||||
}
|
||||
|
||||
public async saveDocFile(docID: string) {
|
||||
var doc = this.docs[docID];
|
||||
var fileName = this.getFileName(doc);
|
||||
var ext = (fileName.split('.', 2) || [])[1] || '';
|
||||
public saveDocFile(doc: MTDocument) {
|
||||
const url = this.getFileURLByDoc(doc, true);
|
||||
const fileName = this.getInputFileName(doc);
|
||||
|
||||
try {
|
||||
let writer = FileManager.chooseSaveFile(fileName, ext, doc.mime_type, doc.size);
|
||||
await writer.ready;
|
||||
|
||||
let promise = this.downloadDoc(docID, writer);
|
||||
promise.then(() => {
|
||||
writer.close();
|
||||
console.log('saved doc', doc);
|
||||
});
|
||||
|
||||
//console.log('got promise from downloadDoc', promise);
|
||||
|
||||
return {promise};
|
||||
} catch(err) {
|
||||
let promise = this.downloadDoc(docID);
|
||||
promise.then((blob) => {
|
||||
FileManager.download(blob, doc.mime_type, fileName)
|
||||
});
|
||||
|
||||
return {promise};
|
||||
}
|
||||
return appDownloadManager.downloadToDisc(fileName, url);
|
||||
}
|
||||
}
|
||||
|
||||
|
115
src/lib/appManagers/appDownloadManager.ts
Normal file
115
src/lib/appManagers/appDownloadManager.ts
Normal file
@ -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();
|
@ -768,16 +768,17 @@ export class AppImManager {
|
||||
window.addEventListener('blur', () => {
|
||||
animationIntersector.checkAnimations(true);
|
||||
|
||||
this.offline = true;
|
||||
this.offline = $rootScope.idle.isIDLE = true;
|
||||
this.updateStatus();
|
||||
clearInterval(this.updateStatusInterval);
|
||||
|
||||
window.addEventListener('focus', () => {
|
||||
animationIntersector.checkAnimations(false);
|
||||
|
||||
this.offline = false;
|
||||
this.offline = $rootScope.idle.isIDLE = false;
|
||||
this.updateStatus();
|
||||
this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3);
|
||||
|
||||
// в обратном порядке
|
||||
animationIntersector.checkAnimations(false);
|
||||
}, {once: true});
|
||||
});
|
||||
|
||||
|
@ -12,7 +12,6 @@ import AvatarElement from "../../components/avatar";
|
||||
import LazyLoadQueue from "../../components/lazyLoadQueue";
|
||||
import appForward from "../../components/appForward";
|
||||
import { isSafari, mediaSizes } from "../config";
|
||||
import MP4Source from "../MP4Source";
|
||||
|
||||
export class AppMediaViewer {
|
||||
public wholeDiv = document.querySelector('.media-viewer-whole') as HTMLDivElement;
|
||||
@ -806,7 +805,7 @@ export class AppMediaViewer {
|
||||
video.append(source);
|
||||
}
|
||||
|
||||
const createPlayer = (streamable = false) => {
|
||||
const createPlayer = () => {
|
||||
if(media.type != 'gif') {
|
||||
video.dataset.ckin = 'default';
|
||||
video.dataset.overlay = '1';
|
||||
@ -815,7 +814,7 @@ export class AppMediaViewer {
|
||||
div.append(video);
|
||||
}
|
||||
|
||||
const player = new VideoPlayer(video, true, streamable);
|
||||
const player = new VideoPlayer(video, true, media.supportsStreaming);
|
||||
return player;
|
||||
/* player.wrapper.parentElement.append(video);
|
||||
mover.append(player.wrapper); */
|
||||
@ -828,60 +827,19 @@ export class AppMediaViewer {
|
||||
const load = () => {
|
||||
const promise = appDocsManager.downloadDoc(media.id);
|
||||
|
||||
const streamable = media.supportsStreaming && !media.url;
|
||||
//if(!streamable) {
|
||||
this.preloader.attach(mover, true, promise);
|
||||
//}
|
||||
|
||||
let player: VideoPlayer;
|
||||
|
||||
let offset = 0;
|
||||
let loadedParts: {[offset: number]: true} = {};
|
||||
|
||||
let preloaderNotify = promise.notify;
|
||||
let promiseNotify = (details: {offset: number, total: number, done: number}) => {
|
||||
if(player) {
|
||||
//player.progress.setLoadProgress(details.done / details.total);
|
||||
setLoadProgress();
|
||||
}
|
||||
|
||||
loadedParts[details.offset] = true;
|
||||
preloaderNotify(details);
|
||||
};
|
||||
if(streamable) {
|
||||
promise.notify = promiseNotify;
|
||||
}
|
||||
|
||||
let setLoadProgress = () => {
|
||||
let rounded = offset - (offset % 524288);
|
||||
|
||||
let downloadedAfter = 0;
|
||||
for(let i in loadedParts) {
|
||||
let o = +i;
|
||||
if(o >= rounded) {
|
||||
downloadedAfter += 524288;
|
||||
}
|
||||
}
|
||||
|
||||
if(offset > rounded) {
|
||||
downloadedAfter -= offset % 524288;
|
||||
}
|
||||
|
||||
player.progress.setLoadProgress(Math.min(1, downloadedAfter / media.size + rounded / media.size));
|
||||
};
|
||||
|
||||
promise.then(async(mp4Source: any) => {
|
||||
promise.then(async() => {
|
||||
if(this.currentMessageID != message.mid) {
|
||||
this.log.warn('media viewer changed video');
|
||||
return;
|
||||
}
|
||||
|
||||
const isStream = mp4Source instanceof MP4Source;
|
||||
if(isStream) {
|
||||
promise.notify = promiseNotify;
|
||||
}
|
||||
|
||||
const url = isStream ? mp4Source.getURL() : media.url;
|
||||
const url = media.url;
|
||||
if(target instanceof SVGSVGElement && (video.parentElement || !isSafari)) { // if video exists
|
||||
if(!video.parentElement) {
|
||||
div.firstElementChild.lastElementChild.append(video);
|
||||
@ -914,16 +872,7 @@ export class AppMediaViewer {
|
||||
});
|
||||
});
|
||||
|
||||
player = createPlayer(streamable);
|
||||
if(player && mp4Source instanceof MP4Source) {
|
||||
player.progress.onSeek = (time) => {
|
||||
//this.log('seek', time);
|
||||
offset = mp4Source.seek(time);
|
||||
setLoadProgress();
|
||||
};
|
||||
|
||||
this.log('lol');
|
||||
}
|
||||
player = createPlayer();
|
||||
});
|
||||
|
||||
return promise;
|
||||
|
@ -1,10 +1,10 @@
|
||||
import appUsersManager from "./appUsersManager";
|
||||
import { calcImageInBox, isObject, getFileURL } from "../utils";
|
||||
import fileManager from '../filemanager';
|
||||
import { bytesFromHex } from "../bin_utils";
|
||||
import { bytesFromHex, getFileNameByLocation } from "../bin_utils";
|
||||
//import apiManager from '../mtproto/apiManager';
|
||||
import apiManager from '../mtproto/mtprotoworker';
|
||||
import { MTPhotoSize, inputPhotoFileLocation, inputDocumentFileLocation, InputFileLocation, FileLocation } from "../../types";
|
||||
import { MTPhotoSize, inputPhotoFileLocation, inputDocumentFileLocation, FileLocation } from "../../types";
|
||||
import appDownloadManager from "./appDownloadManager";
|
||||
|
||||
export type MTPhoto = {
|
||||
_: 'photo' | 'photoEmpty' | string,
|
||||
@ -59,38 +59,6 @@ export class AppPhotosManager {
|
||||
console.warn('no apiPhoto.id', photo);
|
||||
} else this.photos[photo.id] = photo as any;
|
||||
|
||||
/* if(!('sizes' in photo)) return;
|
||||
|
||||
photo.sizes.forEach((photoSize: any) => {
|
||||
if(photoSize._ == 'photoCachedSize') {
|
||||
apiFileManager.saveSmallFile(photoSize.location, photoSize.bytes);
|
||||
|
||||
console.log('clearing photo cached size', photo);
|
||||
|
||||
// Memory
|
||||
photoSize.size = photoSize.bytes.length;
|
||||
delete photoSize.bytes;
|
||||
photoSize._ = 'photoSize';
|
||||
}
|
||||
}); */
|
||||
|
||||
/* if(!photo.downloaded) {
|
||||
photo.downloaded = apiFileManager.isFileExists({
|
||||
_: 'inputPhotoFileLocation',
|
||||
id: photo.id,
|
||||
access_hash: photo.access_hash,
|
||||
file_reference: photo.file_reference
|
||||
});
|
||||
// apiFileManager.isFileExists({
|
||||
// _: 'inputPhotoFileLocation',
|
||||
// id: photo.id,
|
||||
// access_hash: photo.access_hash,
|
||||
// file_reference: photo.file_reference
|
||||
// }).then(downloaded => {
|
||||
// photo.downloaded = downloaded;
|
||||
// });
|
||||
} */
|
||||
|
||||
return photo;
|
||||
}
|
||||
|
||||
@ -326,15 +294,11 @@ export class AppPhotosManager {
|
||||
}
|
||||
|
||||
public downloadPhoto(photoID: string) {
|
||||
var photo = this.photos[photoID];
|
||||
var ext = 'jpg';
|
||||
var mimeType = 'image/jpeg';
|
||||
var fileName = 'photo' + photoID + '.' + ext;
|
||||
var fullWidth = this.windowW;
|
||||
var fullHeight = this.windowH;
|
||||
var fullPhotoSize = this.choosePhotoSize(photo, fullWidth, fullHeight);
|
||||
var inputFileLocation: inputDocumentFileLocation | inputPhotoFileLocation = {
|
||||
// @ts-ignore
|
||||
const photo = this.photos[photoID];
|
||||
const fullWidth = this.windowW;
|
||||
const fullHeight = this.windowH;
|
||||
const fullPhotoSize = this.choosePhotoSize(photo, fullWidth, fullHeight);
|
||||
const location: inputDocumentFileLocation | inputPhotoFileLocation = {
|
||||
_: photo._ == 'document' ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation',
|
||||
id: photo.id,
|
||||
access_hash: photo.access_hash,
|
||||
@ -342,36 +306,10 @@ export class AppPhotosManager {
|
||||
thumb_size: fullPhotoSize.type
|
||||
};
|
||||
|
||||
try { // photo.dc_id, location, photoSize.size
|
||||
let writer = fileManager.chooseSaveFile(fileName, ext, mimeType, fullPhotoSize.size);
|
||||
writer.ready.then(() => {
|
||||
console.log('ready');
|
||||
apiManager.downloadFile(photo.dc_id, inputFileLocation, fullPhotoSize.size, {
|
||||
mimeType: mimeType,
|
||||
toFileEntry: writer
|
||||
}).then(() => {
|
||||
writer.close();
|
||||
//writer.abort();
|
||||
console.log('file save done', fileName, ext, mimeType, writer);
|
||||
}, (e: any) => {
|
||||
console.log('photo download failed', e);
|
||||
});
|
||||
});
|
||||
} catch(err) {
|
||||
console.error('err', err);
|
||||
const url = getFileURL('download', {dcID: photo.dc_id, location, size: fullPhotoSize.size, fileName: 'photo' + photo.id + '.jpg'});
|
||||
const fileName = getFileNameByLocation(location);
|
||||
|
||||
/* var cachedBlob = apiFileManager.getCachedFile(inputFileLocation)
|
||||
if (cachedBlob) {
|
||||
return fileManager.download(cachedBlob, mimeType, fileName);
|
||||
} */
|
||||
|
||||
apiManager.downloadFile(photo.dc_id, inputFileLocation, fullPhotoSize.size, {mimeType: mimeType})
|
||||
.then((blob: Blob) => {
|
||||
fileManager.download(blob, mimeType, fileName);
|
||||
}, (e: any) => {
|
||||
console.log('photo download failed', e);
|
||||
});
|
||||
}
|
||||
appDownloadManager.downloadToDisc(fileName, url);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,5 @@
|
||||
import {blobConstruct} from './bin_utils';
|
||||
|
||||
/* import 'web-streams-polyfill/ponyfill';
|
||||
// @ts-ignore
|
||||
import streamSaver from 'streamsaver';
|
||||
if(window.location.href.indexOf('localhost') === -1) {
|
||||
streamSaver.mitm = 'mitm.html';
|
||||
} */
|
||||
|
||||
class FileManager {
|
||||
public blobSupported = true;
|
||||
|
||||
@ -22,20 +15,6 @@ class FileManager {
|
||||
return this.blobSupported;
|
||||
}
|
||||
|
||||
/* public copy(fromFileEntry: any, toFileEntry: any) {
|
||||
return this.getFileWriter(toFileEntry).then((fileWriter) => {
|
||||
return this.write(fileWriter, fromFileEntry).then(() => {
|
||||
return fileWriter;
|
||||
}, (error: any) => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
fileWriter.truncate(0);
|
||||
} catch (e) {}
|
||||
|
||||
return Promise.reject(error);
|
||||
});
|
||||
});
|
||||
} */
|
||||
public copy(fromFileEntry: any, toFileEntry: any) {
|
||||
return this.write(toFileEntry, fromFileEntry).then(() => {
|
||||
console.log('copy success');
|
||||
@ -52,32 +31,6 @@ class FileManager {
|
||||
});
|
||||
}
|
||||
|
||||
/* public write(fileWriter: any, bytes: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileWriter.onwriteend = function(e: any) {
|
||||
resolve();
|
||||
};
|
||||
fileWriter.onerror = function(e: any) {
|
||||
reject(e);
|
||||
};
|
||||
|
||||
if(bytes.file) {
|
||||
bytes.file((file: any) => {
|
||||
fileWriter.write(file);
|
||||
}, reject);
|
||||
} else if(bytes instanceof Blob) { // is file bytes
|
||||
fileWriter.write(bytes);
|
||||
} else {
|
||||
try {
|
||||
var blob = blobConstruct([bytesToArrayBuffer(bytes)]);
|
||||
fileWriter.write(blob);
|
||||
} catch(e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
} */
|
||||
|
||||
public write(fileWriter: ReturnType<FileManager['getFakeFileWriter']>, bytes: Uint8Array | Blob | {file: any}): Promise<void> {
|
||||
if('file' in bytes) {
|
||||
return bytes.file((file: any) => {
|
||||
@ -97,23 +50,10 @@ class FileManager {
|
||||
fileReader.readAsArrayBuffer(bytes);
|
||||
});
|
||||
} else {
|
||||
//var blob = blobConstruct([bytesToArrayBuffer(bytes)]);
|
||||
//return fileWriter.write(blob);
|
||||
return fileWriter.write(bytes);
|
||||
}
|
||||
}
|
||||
|
||||
public chooseSaveFile(fileName: string, ext: string, mimeType: string, size?: number): any {
|
||||
throw new Error('no writer');
|
||||
/* let fileStream = streamSaver.createWriteStream(fileName, {
|
||||
size: size,
|
||||
writableStrategy: undefined,
|
||||
readableStrategy: undefined
|
||||
});
|
||||
let writer = fileStream.getWriter();
|
||||
return writer; */
|
||||
}
|
||||
|
||||
public getFakeFileWriter(mimeType: string, saveFileCallback: (blob: Blob) => Promise<Blob>) {
|
||||
let blobParts: Array<Uint8Array> = [];
|
||||
const fakeFileWriter = {
|
||||
@ -139,70 +79,6 @@ class FileManager {
|
||||
|
||||
return fakeFileWriter;
|
||||
}
|
||||
|
||||
public download(blob: Blob, mimeType: string, fileName: string) {
|
||||
if(window.navigator && navigator.msSaveBlob !== undefined) {
|
||||
window.navigator.msSaveBlob(blob, fileName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(window.navigator && 'getDeviceStorage' in navigator) {
|
||||
var storageName = 'sdcard';
|
||||
var subdir = 'telegram/';
|
||||
switch(mimeType.split('/')[0]) {
|
||||
case 'video':
|
||||
storageName = 'videos';
|
||||
break;
|
||||
case 'audio':
|
||||
storageName = 'music';
|
||||
break;
|
||||
case 'image':
|
||||
storageName = 'pictures';
|
||||
break;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
var deviceStorage = navigator.getDeviceStorage(storageName);
|
||||
var request = deviceStorage.addNamed(blob, subdir + fileName);
|
||||
|
||||
request.onsuccess = function () {
|
||||
console.log('Device storage save result', this.result);
|
||||
};
|
||||
request.onerror = () => {};
|
||||
return;
|
||||
}
|
||||
|
||||
let url = URL.createObjectURL(blob);
|
||||
var anchor = document.createElementNS('http://www.w3.org/1999/xhtml', 'a') as HTMLAnchorElement;
|
||||
anchor.href = url as string;
|
||||
anchor.download = fileName;
|
||||
if(anchor.dataset) {
|
||||
anchor.dataset.downloadurl = ['video/quicktime', fileName, url].join(':');
|
||||
}
|
||||
|
||||
anchor.style.position = 'absolute';
|
||||
anchor.style.top = '1px';
|
||||
anchor.style.left = '1px';
|
||||
|
||||
document.body.append(anchor);
|
||||
|
||||
try {
|
||||
var clickEvent = document.createEvent('MouseEvents');
|
||||
clickEvent.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
|
||||
anchor.dispatchEvent(clickEvent);
|
||||
} catch (e) {
|
||||
console.error('Download click error', e);
|
||||
try {
|
||||
anchor.click();
|
||||
} catch (e) {
|
||||
window.open(url as string, '_blank');
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
anchor.remove();
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
export default new FileManager();
|
||||
|
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;
|
@ -11,7 +11,7 @@ export class MediaProgressLine {
|
||||
|
||||
public onSeek: (time: number) => void;
|
||||
|
||||
constructor(private media: HTMLAudioElement | HTMLVideoElement, streamable = false) {
|
||||
constructor(private media: HTMLAudioElement | HTMLVideoElement, private streamable = false) {
|
||||
this.container = document.createElement('div');
|
||||
this.container.classList.add('media-progress');
|
||||
|
||||
@ -22,6 +22,7 @@ export class MediaProgressLine {
|
||||
this.filledLoad = document.createElement('div');
|
||||
this.filledLoad.classList.add('media-progress__filled', 'media-progress__loaded');
|
||||
this.container.append(this.filledLoad);
|
||||
//this.setLoadProgress();
|
||||
}
|
||||
|
||||
let seek = this.seek = document.createElement('input');
|
||||
@ -62,6 +63,10 @@ export class MediaProgressLine {
|
||||
window.cancelAnimationFrame(this.progressRAF);
|
||||
}
|
||||
|
||||
if(this.streamable) {
|
||||
this.setLoadProgress();
|
||||
}
|
||||
|
||||
this.progressRAF = window.requestAnimationFrame(r);
|
||||
};
|
||||
|
||||
@ -96,7 +101,29 @@ export class MediaProgressLine {
|
||||
this.mousedown = false;
|
||||
};
|
||||
|
||||
public setLoadProgress(percents: number) {
|
||||
onProgress = (e: Event) => {
|
||||
this.setLoadProgress();
|
||||
};
|
||||
|
||||
private setLoadProgress() {
|
||||
const buf = this.media.buffered;
|
||||
const numRanges = buf.length;
|
||||
|
||||
const currentTime = this.media.currentTime;
|
||||
let nearestStart = 0, end = 0;
|
||||
for(let i = 0; i < numRanges; ++i) {
|
||||
const start = buf.start(i);
|
||||
if(currentTime >= start && start >= nearestStart) {
|
||||
nearestStart = start;
|
||||
end = buf.end(i);
|
||||
}
|
||||
|
||||
//console.log('onProgress range:', i, buf.start(i), buf.end(i), this.media);
|
||||
}
|
||||
|
||||
//console.log('onProgress correct range:', nearestStart, end, this.media);
|
||||
|
||||
const percents = this.media.duration ? end / this.media.duration : 0;
|
||||
this.filledLoad.style.transform = 'scaleX(' + percents + ')';
|
||||
}
|
||||
|
||||
@ -120,6 +147,7 @@ export class MediaProgressLine {
|
||||
private setListeners() {
|
||||
this.media.addEventListener('ended', this.onEnded);
|
||||
this.media.addEventListener('play', this.onPlay);
|
||||
this.streamable && this.media.addEventListener('progress', this.onProgress);
|
||||
|
||||
this.container.addEventListener('mousemove', this.onMouseMove);
|
||||
this.container.addEventListener('mousedown', this.onMouseDown);
|
||||
@ -143,6 +171,7 @@ export class MediaProgressLine {
|
||||
this.media.removeEventListener('loadeddata', this.onLoadedData);
|
||||
this.media.removeEventListener('ended', this.onEnded);
|
||||
this.media.removeEventListener('play', this.onPlay);
|
||||
this.streamable && this.media.removeEventListener('progress', this.onProgress);
|
||||
|
||||
this.container.removeEventListener('mousemove', this.onMouseMove);
|
||||
this.container.removeEventListener('mousedown', this.onMouseDown);
|
||||
|
@ -6,7 +6,7 @@ import apiManager from "./apiManager";
|
||||
import { deferredPromise, CancellablePromise } from "../polyfill";
|
||||
import appWebpManager from "../appManagers/appWebpManager";
|
||||
import { logger } from "../logger";
|
||||
import { InputFileLocation, FileLocation } from "../../types";
|
||||
import { InputFileLocation, FileLocation, UploadFile } from "../../types";
|
||||
|
||||
type Delayed = {
|
||||
offset: number,
|
||||
@ -14,18 +14,25 @@ type Delayed = {
|
||||
writeFileDeferred: CancellablePromise<unknown>
|
||||
};
|
||||
|
||||
type DownloadOptions = Partial<{
|
||||
|
||||
}>;
|
||||
export type DownloadOptions = {
|
||||
dcID: number,
|
||||
location: InputFileLocation | FileLocation,
|
||||
size: number,
|
||||
fileName?: string,
|
||||
mimeType?: string,
|
||||
limitPart?: number,
|
||||
stickerType?: number,
|
||||
processPart?: (bytes: Uint8Array, offset?: number, queue?: Delayed[]) => Promise<any>
|
||||
};
|
||||
|
||||
export class ApiFileManager {
|
||||
public cachedDownloadPromises: {
|
||||
[fileName: string]: Promise<Blob>
|
||||
[fileName: string]: CancellablePromise<Blob>
|
||||
} = {};
|
||||
|
||||
public downloadPulls: {
|
||||
[x: string]: Array<{
|
||||
cb: () => Promise<unknown>,
|
||||
cb: () => Promise<UploadFile | void>,
|
||||
deferred: {
|
||||
resolve: (...args: any[]) => void,
|
||||
reject: (...args: any[]) => void
|
||||
@ -37,7 +44,9 @@ export class ApiFileManager {
|
||||
|
||||
private log: ReturnType<typeof logger> = logger('AFM');
|
||||
|
||||
public downloadRequest(dcID: string | number, cb: () => Promise<unknown>, activeDelta: number) {
|
||||
public downloadRequest(dcID: 'upload', cb: () => Promise<void>, activeDelta: number): Promise<void>;
|
||||
public downloadRequest(dcID: number, cb: () => Promise<UploadFile>, activeDelta: number): Promise<UploadFile>;
|
||||
public downloadRequest(dcID: number | string, cb: () => Promise<UploadFile | void>, activeDelta: number) {
|
||||
if(this.downloadPulls[dcID] === undefined) {
|
||||
this.downloadPulls[dcID] = [];
|
||||
this.downloadActives[dcID] = 0;
|
||||
@ -45,9 +54,9 @@ export class ApiFileManager {
|
||||
|
||||
const downloadPull = this.downloadPulls[dcID];
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
downloadPull.push({cb: cb, deferred: {resolve, reject}, activeDelta: activeDelta});
|
||||
})/* .catch(() => {}) */;
|
||||
const promise = new Promise<UploadFile | void>((resolve, reject) => {
|
||||
downloadPull.push({cb, deferred: {resolve, reject}, activeDelta});
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.downloadCheck(dcID);
|
||||
@ -60,7 +69,7 @@ export class ApiFileManager {
|
||||
const downloadPull = this.downloadPulls[dcID];
|
||||
//const downloadLimit = dcID == 'upload' ? 11 : 5;
|
||||
//const downloadLimit = 24;
|
||||
const downloadLimit = dcID == 'upload' ? 11 : 24;
|
||||
const downloadLimit = dcID == 'upload' ? 11 : 50;
|
||||
|
||||
if(this.downloadActives[dcID] >= downloadLimit || !downloadPull || !downloadPull.length) {
|
||||
return false;
|
||||
@ -72,12 +81,12 @@ export class ApiFileManager {
|
||||
this.downloadActives[dcID] += activeDelta;
|
||||
|
||||
data.cb()
|
||||
.then((result: any) => {
|
||||
.then((result) => {
|
||||
this.downloadActives[dcID] -= activeDelta;
|
||||
this.downloadCheck(dcID);
|
||||
|
||||
data.deferred.resolve(result);
|
||||
}, (error: any) => {
|
||||
}, (error: Error) => {
|
||||
if(error) {
|
||||
this.log.error('downloadCheck error:', error);
|
||||
}
|
||||
@ -93,16 +102,48 @@ export class ApiFileManager {
|
||||
return cacheStorage;
|
||||
}
|
||||
|
||||
public downloadFile(options: {
|
||||
dcID: number,
|
||||
location: InputFileLocation | FileLocation,
|
||||
size: number,
|
||||
mimeType?: string,
|
||||
toFileEntry?: any,
|
||||
limitPart?: number,
|
||||
stickerType?: number,
|
||||
processPart?: (bytes: Uint8Array, offset: number, queue: Delayed[]) => Promise<any>
|
||||
}): CancellablePromise<Blob> {
|
||||
public cancelDownload(fileName: string) {
|
||||
const promise = this.cachedDownloadPromises[fileName];
|
||||
if(promise) {
|
||||
promise.cancel();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public requestFilePart(dcID: number, location: InputFileLocation | FileLocation, offset: number, limit: number, checkCancel?: () => void) {
|
||||
const delta = limit / 1024 / 256;
|
||||
return this.downloadRequest(dcID, async() => {
|
||||
checkCancel && checkCancel();
|
||||
|
||||
return apiManager.invokeApi('upload.getFile', {
|
||||
location,
|
||||
offset,
|
||||
limit
|
||||
}, {
|
||||
dcID,
|
||||
fileDownload: true/* ,
|
||||
singleInRequest: 'safari' in window */
|
||||
}) as Promise<UploadFile>;
|
||||
}, delta);
|
||||
}
|
||||
|
||||
private convertBlobToBytes(blob: Blob) {
|
||||
return blob.arrayBuffer().then(buffer => new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
private getLimitPart(size: number): number {
|
||||
let bytes: number;
|
||||
|
||||
if(size < 1e6 || !size) bytes = 512;
|
||||
else if(size < 3e6) bytes = 256;
|
||||
else bytes = 128;
|
||||
|
||||
return bytes * 1024;
|
||||
}
|
||||
|
||||
public downloadFile(options: DownloadOptions): CancellablePromise<Blob> {
|
||||
if(!FileManager.isAvailable()) {
|
||||
return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'});
|
||||
}
|
||||
@ -112,7 +153,7 @@ export class ApiFileManager {
|
||||
|
||||
let processSticker = false;
|
||||
if(options.stickerType == 1 && !appWebpManager.isSupported()) {
|
||||
if(options.toFileEntry || size > 524288) {
|
||||
if(size > 524288) {
|
||||
delete options.stickerType;
|
||||
} else {
|
||||
processSticker = true;
|
||||
@ -122,16 +163,18 @@ export class ApiFileManager {
|
||||
|
||||
// this.log('Dload file', dcID, location, size)
|
||||
const fileName = getFileNameByLocation(location);
|
||||
const toFileEntry = options.toFileEntry || null;
|
||||
const cachedPromise = this.cachedDownloadPromises[fileName];
|
||||
const fileStorage = this.getFileStorage();
|
||||
|
||||
//this.log('downloadFile', fileName, fileName.length, location, arguments);
|
||||
|
||||
if(cachedPromise) {
|
||||
if(toFileEntry) {
|
||||
return cachedPromise.then((blob: any) => {
|
||||
return FileManager.copy(blob, toFileEntry);
|
||||
if(options.processPart) {
|
||||
return cachedPromise.then((blob) => {
|
||||
return this.convertBlobToBytes(blob).then(bytes => {
|
||||
options.processPart(bytes)
|
||||
return blob;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -181,30 +224,28 @@ export class ApiFileManager {
|
||||
throw false;
|
||||
}
|
||||
|
||||
if(toFileEntry) {
|
||||
FileManager.copy(blob, toFileEntry).then(deferred.resolve, errorHandler);
|
||||
} else {
|
||||
deferred.resolve(blob);
|
||||
if(options.processPart) {
|
||||
//FileManager.copy(blob, toFileEntry).then(deferred.resolve, errorHandler);
|
||||
await this.convertBlobToBytes(blob).then(bytes => {
|
||||
options.processPart(bytes);
|
||||
});
|
||||
}
|
||||
|
||||
deferred.resolve(blob);
|
||||
}).catch(() => {
|
||||
//this.log('not cached', fileName);
|
||||
//var fileWriterPromise = toFileEntry ? FileManager.getFileWriter(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType);
|
||||
const fileWriterPromise = toFileEntry ? Promise.resolve(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType);
|
||||
const fileWriterPromise = fileStorage.getFileWriter(fileName, mimeType);
|
||||
|
||||
fileWriterPromise.then((fileWriter: any) => {
|
||||
fileWriterPromise.then((fileWriter) => {
|
||||
cacheFileWriter = fileWriter;
|
||||
const limit = options.limitPart || 524288;
|
||||
const limit = options.limitPart || this.getLimitPart(size);
|
||||
let offset: number;
|
||||
let startOffset = 0;
|
||||
let writeFilePromise: CancellablePromise<unknown> = Promise.resolve(),
|
||||
writeFileDeferred: CancellablePromise<unknown>;
|
||||
const maxRequests = options.processPart ? 5 : 5;
|
||||
const maxRequests = options.processPart ? 5 : 10;
|
||||
|
||||
if(!size) {
|
||||
size = limit;
|
||||
}
|
||||
|
||||
if(fileWriter.length) {
|
||||
/* if(fileWriter.length) {
|
||||
startOffset = fileWriter.length;
|
||||
|
||||
if(startOffset >= size) {
|
||||
@ -221,7 +262,7 @@ export class ApiFileManager {
|
||||
deferred.notify({done: startOffset, total: size});
|
||||
|
||||
/////this.log('deferred notify 1:', {done: startOffset, total: size});
|
||||
}
|
||||
} */
|
||||
|
||||
const processDownloaded = async(bytes: Uint8Array, offset: number) => {
|
||||
if(options.processPart) {
|
||||
@ -236,18 +277,20 @@ export class ApiFileManager {
|
||||
};
|
||||
|
||||
const delayed: Delayed[] = [];
|
||||
for(offset = startOffset; offset < size; offset += limit) {
|
||||
offset = startOffset;
|
||||
do {
|
||||
////this.log('offset:', startOffset);
|
||||
writeFileDeferred = deferredPromise<void>();
|
||||
delayed.push({offset, writeFilePromise, writeFileDeferred});
|
||||
writeFilePromise = writeFileDeferred;
|
||||
////this.log('offset:', startOffset);
|
||||
}
|
||||
offset += limit;
|
||||
} while(offset < size);
|
||||
|
||||
// для потокового видео нужно скачать первый и последний чанки
|
||||
if(options.processPart && delayed.length > 2) {
|
||||
/* if(options.processPart && delayed.length > 2) {
|
||||
const last = delayed.splice(delayed.length - 1, 1)[0];
|
||||
delayed.splice(1, 0, last);
|
||||
}
|
||||
} */
|
||||
|
||||
// @ts-ignore
|
||||
//deferred.queue = delayed;
|
||||
@ -258,21 +301,7 @@ export class ApiFileManager {
|
||||
|
||||
const {offset, writeFilePromise, writeFileDeferred} = delayed.shift();
|
||||
try {
|
||||
const result: any = await this.downloadRequest(dcID, () => {
|
||||
if(canceled) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return apiManager.invokeApi('upload.getFile', {
|
||||
location,
|
||||
offset,
|
||||
limit
|
||||
}, {
|
||||
dcID,
|
||||
fileDownload: true/* ,
|
||||
singleInRequest: 'safari' in window */
|
||||
});
|
||||
}, 2);
|
||||
const result = await this.requestFilePart(dcID, location, offset, limit, checkCancel);
|
||||
|
||||
if(delayed.length) {
|
||||
superpuper();
|
||||
@ -280,11 +309,10 @@ export class ApiFileManager {
|
||||
|
||||
//////////////////////////////////////////
|
||||
const processedResult = await processDownloaded(result.bytes, offset);
|
||||
if(canceled) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
checkCancel();
|
||||
|
||||
done += limit;
|
||||
//done += limit;
|
||||
done += processedResult.byteLength;
|
||||
const isFinal = offset + limit >= size;
|
||||
//if(!isFinal) {
|
||||
////this.log('deferred notify 2:', {done: offset + limit, total: size}, deferred);
|
||||
@ -292,9 +320,7 @@ export class ApiFileManager {
|
||||
//}
|
||||
|
||||
await writeFilePromise;
|
||||
if(canceled) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
checkCancel();
|
||||
|
||||
await FileManager.write(fileWriter, processedResult);
|
||||
writeFileDeferred.resolve();
|
||||
@ -302,7 +328,7 @@ export class ApiFileManager {
|
||||
if(isFinal) {
|
||||
resolved = true;
|
||||
|
||||
if(toFileEntry) {
|
||||
if(options.processPart) {
|
||||
deferred.resolve();
|
||||
} else {
|
||||
deferred.resolve(fileWriter.finalize());
|
||||
@ -319,22 +345,21 @@ export class ApiFileManager {
|
||||
});
|
||||
});
|
||||
|
||||
const checkCancel = () => {
|
||||
if(canceled) {
|
||||
throw new Error('canceled');
|
||||
}
|
||||
};
|
||||
|
||||
deferred.cancel = () => {
|
||||
if(!canceled && !resolved) {
|
||||
canceled = true;
|
||||
delete this.cachedDownloadPromises[fileName];
|
||||
errorHandler({type: 'DOWNLOAD_CANCELED'});
|
||||
if(toFileEntry) {
|
||||
toFileEntry.abort();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//console.log(deferred, deferred.notify, deferred.cancel);
|
||||
|
||||
if(!toFileEntry) {
|
||||
this.cachedDownloadPromises[fileName] = deferred;
|
||||
}
|
||||
this.cachedDownloadPromises[fileName] = deferred;
|
||||
|
||||
return deferred;
|
||||
}
|
||||
|
@ -6,7 +6,11 @@ import apiManager from "./apiManager";
|
||||
import AppStorage from '../storage';
|
||||
import cryptoWorker from "../crypto/cryptoworker";
|
||||
import networkerFactory from "./networkerFactory";
|
||||
import apiFileManager from './apiFileManager';
|
||||
import apiFileManager, { DownloadOptions } from './apiFileManager';
|
||||
import { getFileNameByLocation } from '../bin_utils';
|
||||
import { logger, LogLevels } from '../logger';
|
||||
|
||||
const log = logger('SW'/* , LogLevels.error */);
|
||||
|
||||
const ctx = self as any as ServiceWorkerGlobalScope;
|
||||
|
||||
@ -75,11 +79,10 @@ function respond(client: Client | ServiceWorker | MessagePort, ...args: any[]) {
|
||||
} */
|
||||
}
|
||||
|
||||
networkerFactory.setUpdatesProcessor((obj, bool) => {
|
||||
//console.log('updatesss');
|
||||
//ctx.postMessage({update: {obj, bool}});
|
||||
//respond({update: {obj, bool}});
|
||||
|
||||
/**
|
||||
* Broadcast Notification
|
||||
*/
|
||||
function notify(...args: any[]) {
|
||||
ctx.clients.matchAll({ includeUncontrolled: false, type: 'window' }).then((listeners) => {
|
||||
if(!listeners.length) {
|
||||
//console.trace('no listeners?', self, listeners);
|
||||
@ -87,15 +90,20 @@ networkerFactory.setUpdatesProcessor((obj, bool) => {
|
||||
}
|
||||
|
||||
listeners.forEach(listener => {
|
||||
listener.postMessage({update: {obj, bool}});
|
||||
// @ts-ignore
|
||||
listener.postMessage(...args);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
networkerFactory.setUpdatesProcessor((obj, bool) => {
|
||||
notify({update: {obj, bool}});
|
||||
});
|
||||
|
||||
ctx.addEventListener('message', async(e) => {
|
||||
const taskID = e.data.taskID;
|
||||
|
||||
//console.log('[SW] Got message:', taskID, e, e.data);
|
||||
log.debug('got message:', taskID, e, e.data);
|
||||
|
||||
if(e.data.useLs) {
|
||||
AppStorage.finishTask(e.data.taskID, e.data.args);
|
||||
@ -110,6 +118,7 @@ ctx.addEventListener('message', async(e) => {
|
||||
respond(e.source, {taskID: taskID, result: result});
|
||||
});
|
||||
|
||||
case 'cancelDownload':
|
||||
case 'downloadFile': {
|
||||
/* // @ts-ignore
|
||||
return apiFileManager.downloadFile(...e.data.args); */
|
||||
@ -151,7 +160,7 @@ ctx.addEventListener('message', async(e) => {
|
||||
* Service Worker Installation
|
||||
*/
|
||||
ctx.addEventListener('install', (event: ExtendableEvent) => {
|
||||
//console.log('service worker is installing');
|
||||
log('installing');
|
||||
|
||||
/* initCache();
|
||||
|
||||
@ -165,7 +174,7 @@ ctx.addEventListener('install', (event: ExtendableEvent) => {
|
||||
* Service Worker Activation
|
||||
*/
|
||||
ctx.addEventListener('activate', (event) => {
|
||||
//console.log('service worker activating', ctx);
|
||||
log('activating', ctx);
|
||||
|
||||
/* if (!ctx.cache) initCache();
|
||||
if (!ctx.network) initNetwork(); */
|
||||
@ -184,28 +193,168 @@ function timeout(delay: number): Promise<Response> {
|
||||
}));
|
||||
}
|
||||
|
||||
ctx.addEventListener('error', (error) => {
|
||||
log.error('error:', error);
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch requests
|
||||
*/
|
||||
ctx.addEventListener('fetch', (event: FetchEvent): void => {
|
||||
const [, url, scope, fileName] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || [];
|
||||
const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || [];
|
||||
|
||||
//console.log('[SW] fetch:', event, event.request, url, scope, fileName);
|
||||
log.debug('[fetch]:', event);
|
||||
|
||||
switch(scope) {
|
||||
case 'download':
|
||||
case 'thumb':
|
||||
case 'document':
|
||||
case 'photo': {
|
||||
const info = JSON.parse(decodeURIComponent(fileName));
|
||||
const info: DownloadOptions = JSON.parse(decodeURIComponent(params));
|
||||
const fileName = getFileNameByLocation(info.location);
|
||||
|
||||
//console.log('[SW] fetch cachedDownloadPromises:', info/* apiFileManager.cachedDownloadPromises, apiFileManager.cachedDownloadPromises.hasOwnProperty(fileName) */);
|
||||
/* event.request.signal.addEventListener('abort', (e) => {
|
||||
console.log('[SW] user aborted request:', fileName);
|
||||
cancellablePromise.cancel();
|
||||
});
|
||||
|
||||
const promise = apiFileManager.downloadFile(info).then(b => new Response(b));
|
||||
event.respondWith(promise);
|
||||
event.request.signal.onabort = (e) => {
|
||||
console.log('[SW] user aborted request:', fileName);
|
||||
cancellablePromise.cancel();
|
||||
};
|
||||
|
||||
if(fileName == '5452060085729624717') {
|
||||
setInterval(() => {
|
||||
console.log('[SW] request status:', fileName, event.request.signal.aborted);
|
||||
}, 1000);
|
||||
} */
|
||||
|
||||
const cancellablePromise = apiFileManager.downloadFile(info);
|
||||
cancellablePromise.notify = (progress: {done: number, total: number, offset: number}) => {
|
||||
notify({progress: {fileName, ...progress}});
|
||||
};
|
||||
|
||||
log.debug('[fetch] file:', /* info, */fileName);
|
||||
|
||||
const promise = cancellablePromise.then(b => new Response(b));
|
||||
event.respondWith(Promise.race([
|
||||
timeout(45 * 1000),
|
||||
promise
|
||||
]));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 'stream': {
|
||||
const [offset, end] = parseRange(event.request.headers.get('Range'));
|
||||
|
||||
const info: DownloadOptions = JSON.parse(decodeURIComponent(params));
|
||||
//const fileName = getFileNameByLocation(info.location);
|
||||
|
||||
log.debug('[stream]', url, offset, end);
|
||||
|
||||
event.respondWith(new Promise((resolve, reject) => {
|
||||
// safari workaround
|
||||
if(offset === 0 && end === 1) {
|
||||
resolve(new Response(new Uint8Array(2).buffer, {
|
||||
status: 206,
|
||||
statusText: 'Partial Content',
|
||||
headers: {
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Range': `bytes 0-1/${info.size || '*'}`,
|
||||
'Content-Length': '2',
|
||||
'Content-Type': info.mimeType || 'video/mp4',
|
||||
},
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = end && end < STREAM_CHUNK_UPPER_LIMIT ? alignLimit(end - offset + 1) : STREAM_CHUNK_UPPER_LIMIT;
|
||||
const alignedOffset = alignOffset(offset, limit);
|
||||
|
||||
//log.debug('[stream] requestFilePart:', info.dcID, info.location, alignedOffset, limit);
|
||||
|
||||
apiFileManager.requestFilePart(info.dcID, info.location, alignedOffset, limit).then(result => {
|
||||
let ab = result.bytes;
|
||||
|
||||
//log.debug('[stream] requestFilePart result:', result);
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Range': `bytes ${alignedOffset}-${alignedOffset + ab.byteLength - 1}/${info.size || '*'}`,
|
||||
'Content-Length': `${ab.byteLength}`,
|
||||
};
|
||||
|
||||
if(info.mimeType) headers['Content-Type'] = info.mimeType;
|
||||
|
||||
if(isSafari(ctx)) {
|
||||
ab = ab.slice(offset - alignedOffset, end - alignedOffset + 1);
|
||||
headers['Content-Range'] = `bytes ${offset}-${offset + ab.byteLength - 1}/${info.size || '*'}`;
|
||||
headers['Content-Length'] = `${ab.byteLength}`;
|
||||
}
|
||||
|
||||
resolve(new Response(ab, {
|
||||
status: 206,
|
||||
statusText: 'Partial Content',
|
||||
headers,
|
||||
}));
|
||||
});
|
||||
}));
|
||||
break;
|
||||
}
|
||||
|
||||
/* case 'download': {
|
||||
const info: DownloadOptions = JSON.parse(decodeURIComponent(params));
|
||||
|
||||
const promise = new Promise<Response>((resolve) => {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Disposition': `attachment; filename="${info.fileName}"`,
|
||||
};
|
||||
|
||||
if(info.size) headers['Content-Length'] = info.size.toString();
|
||||
if(info.mimeType) headers['Content-Type'] = info.mimeType;
|
||||
|
||||
log('[download] file:', info);
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller: ReadableStreamDefaultController) {
|
||||
const limitPart = DOWNLOAD_CHUNK_LIMIT;
|
||||
|
||||
apiFileManager.downloadFile({
|
||||
...info,
|
||||
limitPart,
|
||||
processPart: (bytes, offset) => {
|
||||
log('[download] file processPart:', bytes, offset);
|
||||
|
||||
controller.enqueue(new Uint8Array(bytes));
|
||||
|
||||
const isFinal = offset + limitPart >= info.size;
|
||||
if(isFinal) {
|
||||
controller.close();
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}).catch(err => {
|
||||
log.error('[download] error:', err);
|
||||
controller.error(err);
|
||||
});
|
||||
},
|
||||
|
||||
cancel() {
|
||||
log.error('[download] file canceled:', info);
|
||||
}
|
||||
});
|
||||
|
||||
resolve(new Response(stream, {headers}));
|
||||
});
|
||||
|
||||
event.respondWith(promise);
|
||||
|
||||
break;
|
||||
} */
|
||||
|
||||
case 'upload': {
|
||||
if(event.request.method == 'POST') {
|
||||
event.respondWith(event.request.blob().then(blob => {
|
||||
@ -273,3 +422,24 @@ ctx.addEventListener('fetch', (event: FetchEvent): void => {
|
||||
else event.respondWith(fetch(event.request.url)); */
|
||||
}
|
||||
});
|
||||
|
||||
const DOWNLOAD_CHUNK_LIMIT = 512 * 1024;
|
||||
const STREAM_CHUNK_UPPER_LIMIT = 256 * 1024;
|
||||
const SMALLEST_CHUNK_LIMIT = 256 * 4;
|
||||
|
||||
function parseRange(header: string): [number, number] {
|
||||
if(!header) return [0, 0];
|
||||
const [, chunks] = header.split('=');
|
||||
const ranges = chunks.split(', ');
|
||||
const [offset, end] = ranges[0].split('-');
|
||||
|
||||
return [+offset, +end || 0];
|
||||
}
|
||||
|
||||
function alignOffset(offset: number, base = SMALLEST_CHUNK_LIMIT) {
|
||||
return offset - (offset % base);
|
||||
}
|
||||
|
||||
function alignLimit(limit: number) {
|
||||
return 2 ** Math.ceil(Math.log(limit) / Math.log(2));
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
import {dT, isObject, $rootScope} from '../utils';
|
||||
import {isObject, $rootScope} from '../utils';
|
||||
import AppStorage from '../storage';
|
||||
import CryptoWorkerMethods from '../crypto/crypto_methods';
|
||||
import runtime from 'serviceworker-webpack-plugin/lib/runtime';
|
||||
import { InputFileLocation, FileLocation } from '../../types';
|
||||
import { logger } from '../logger';
|
||||
|
||||
type Task = {
|
||||
taskID: number,
|
||||
@ -10,14 +11,6 @@ type Task = {
|
||||
args: any[]
|
||||
};
|
||||
|
||||
/* let pending: any[] = [];
|
||||
function resendPending() {
|
||||
if(navigator.serviceWorker.controller) {
|
||||
for(let i = 0; i < pending.length; i++) navigator.serviceWorker.controller.postMessage(pending[i]);
|
||||
pending = [];
|
||||
}
|
||||
} */
|
||||
|
||||
class ApiManagerProxy extends CryptoWorkerMethods {
|
||||
private taskID = 0;
|
||||
private awaiting: {
|
||||
@ -28,30 +21,37 @@ class ApiManagerProxy extends CryptoWorkerMethods {
|
||||
}
|
||||
} = {} as any;
|
||||
private pending: Array<Task> = [];
|
||||
private debug = false;
|
||||
|
||||
public updatesProcessor: (obj: any, bool: boolean) => void = null;
|
||||
|
||||
private log = logger('API-PROXY');
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
console.log(dT(), 'ApiManagerProxy constructor');
|
||||
this.log('constructor');
|
||||
|
||||
/**
|
||||
* Service worker
|
||||
*/
|
||||
runtime.register({ scope: '/' });
|
||||
(runtime.register({ scope: '/' }) as Promise<ServiceWorkerRegistration>).then(registration => {
|
||||
|
||||
}, (err) => {
|
||||
this.log.error('SW registration failed!', err);
|
||||
});
|
||||
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
console.info(dT(), 'ApiManagerProxy set SW');
|
||||
this.log('set SW');
|
||||
this.releasePending();
|
||||
|
||||
//registration.update();
|
||||
});
|
||||
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
console.warn(dT(), 'ApiManagerProxy controllerchange');
|
||||
this.log.warn('controllerchange');
|
||||
this.releasePending();
|
||||
|
||||
navigator.serviceWorker.controller.addEventListener('error', (e) => {
|
||||
console.error('controller error:', e);
|
||||
this.log.error('controller error:', e);
|
||||
});
|
||||
});
|
||||
|
||||
@ -72,6 +72,8 @@ class ApiManagerProxy extends CryptoWorkerMethods {
|
||||
if(this.updatesProcessor) {
|
||||
this.updatesProcessor(e.data.update.obj, e.data.update.bool);
|
||||
}
|
||||
} else if(e.data.progress) {
|
||||
$rootScope.$broadcast('download_progress', e.data.progress);
|
||||
} else {
|
||||
this.finalizeTask(e.data.taskID, e.data.result, e.data.error);
|
||||
}
|
||||
@ -81,14 +83,14 @@ class ApiManagerProxy extends CryptoWorkerMethods {
|
||||
private finalizeTask(taskID: number, result: any, error: any) {
|
||||
let deferred = this.awaiting[taskID];
|
||||
if(deferred !== undefined) {
|
||||
this.debug && console.log(dT(), 'ApiManagerProxy done', deferred.taskName, result, error);
|
||||
this.log.debug('done', deferred.taskName, result, error);
|
||||
result === undefined ? deferred.reject(error) : deferred.resolve(result);
|
||||
delete this.awaiting[taskID];
|
||||
}
|
||||
}
|
||||
|
||||
public performTaskWorker<T>(task: string, ...args: any[]) {
|
||||
this.debug && console.log(dT(), 'ApiManagerProxy start', task, args);
|
||||
this.log.debug('start', task, args);
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
this.awaiting[this.taskID] = {resolve, reject, taskName: task};
|
||||
@ -168,6 +170,10 @@ class ApiManagerProxy extends CryptoWorkerMethods {
|
||||
}> = {}): Promise<Blob> {
|
||||
return this.performTaskWorker('downloadFile', dcID, location, size, options);
|
||||
}
|
||||
|
||||
public cancelDownload(fileName: string) {
|
||||
return this.performTaskWorker('cancelDownload', fileName);
|
||||
}
|
||||
}
|
||||
|
||||
const apiManagerProxy = new ApiManagerProxy();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { bytesToHex, bytesFromHex, dT, bufferConcats } from "./bin_utils";
|
||||
import { bytesToHex, bytesFromHex, bufferConcats } from "./bin_utils";
|
||||
// @ts-ignore
|
||||
import {SecureRandom} from 'jsbn';
|
||||
|
||||
@ -77,16 +77,16 @@ Array.prototype.findAndSplice = function<T>(verify: (value: T, index?: number, a
|
||||
};
|
||||
|
||||
String.prototype.toHHMMSS = function(leadZero = false) {
|
||||
let sec_num = parseInt(this + '', 10);
|
||||
let hours: any = Math.floor(sec_num / 3600);
|
||||
const sec_num = parseInt(this + '', 10);
|
||||
const hours = Math.floor(sec_num / 3600);
|
||||
let minutes: any = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||
let seconds: any = sec_num - (hours * 3600) - (minutes * 60);
|
||||
|
||||
if(hours < 10) hours = "0" + hours;
|
||||
if(hours) leadZero = true;
|
||||
if(minutes < 10) minutes = leadZero ? "0" + minutes : minutes;
|
||||
if(seconds < 10) seconds = "0" + seconds;
|
||||
return minutes + ':' + seconds;
|
||||
}
|
||||
return (hours ? /* ('0' + hours).slice(-2) */hours + ':' : '') + minutes + ':' + seconds;
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface Uint8Array {
|
||||
|
@ -151,8 +151,18 @@ export function getRichElementValue(node: any, lines: string[], line: string[],
|
||||
}
|
||||
} */
|
||||
|
||||
type BroadcastKeys = 'download_progress' | 'user_update' | 'user_auth' | 'peer_changed' |
|
||||
'filter_delete' | 'filter_update' | 'message_edit' | 'dialog_draft' | 'messages_pending' |
|
||||
'history_append' | 'history_update' | 'dialogs_multiupdate' | 'dialog_unread' | 'dialog_flush' |
|
||||
'dialog_drop' | 'dialog_migrate' | 'dialog_top' | 'history_reply_markup' | 'history_multiappend' |
|
||||
'messages_read' | 'history_delete' | 'history_forbidden' | 'history_reload' | 'message_views' |
|
||||
'message_sent' | 'history_request' | 'messages_downloaded' | 'contacts_update' | 'avatar_update' |
|
||||
'stickers_installed' | 'stickers_deleted' | 'chat_full_update' | 'peer_pinned_message' |
|
||||
'poll_update' | 'dialogs_archived_unread' | 'audio_play' | 'audio_pause' | 'chat_update' |
|
||||
'apiUpdate' | 'stateSynchronized' | 'channel_settings' | 'webpage_updated' | 'draft_updated';
|
||||
|
||||
export const $rootScope = {
|
||||
$broadcast: (name: string, detail?: any) => {
|
||||
$broadcast: (name: BroadcastKeys, detail?: any) => {
|
||||
if(name != 'user_update') {
|
||||
console.debug(dT(), 'Broadcasting ' + name + ' event, with args:', detail);
|
||||
}
|
||||
@ -160,9 +170,14 @@ export const $rootScope = {
|
||||
let myCustomEvent = new CustomEvent(name, {detail});
|
||||
document.dispatchEvent(myCustomEvent);
|
||||
},
|
||||
$on: (name: string, callback: any) => {
|
||||
$on: (name: BroadcastKeys, callback: (e: CustomEvent) => any) => {
|
||||
// @ts-ignore
|
||||
document.addEventListener(name, callback);
|
||||
},
|
||||
$off: (name: BroadcastKeys, callback: (e: CustomEvent) => any) => {
|
||||
// @ts-ignore
|
||||
document.removeEventListener(name, callback);
|
||||
},
|
||||
|
||||
selectedPeerID: 0,
|
||||
myID: 0,
|
||||
@ -523,11 +538,12 @@ export function getEmojiToneIndex(input: string) {
|
||||
return match ? 5 - (57343 - match[0].charCodeAt(0)) : 0;
|
||||
}
|
||||
|
||||
export function getFileURL(type: 'photo' | 'thumb' | 'document', options: {
|
||||
export function getFileURL(type: 'photo' | 'thumb' | 'document' | 'stream' | 'download', options: {
|
||||
dcID: number,
|
||||
location: InputFileLocation | FileLocation,
|
||||
size?: number,
|
||||
mimeType?: string
|
||||
mimeType?: string,
|
||||
fileName?: string
|
||||
}) {
|
||||
//console.log('getFileURL', location);
|
||||
//const perf = performance.now();
|
||||
|
@ -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});
|
||||
}
|
||||
});
|
@ -184,10 +184,8 @@ html.no-touch .default.is-playing:hover .default__controls {
|
||||
|
||||
&__loaded {
|
||||
background: rgba(255, 255, 255, 0.38);
|
||||
position: absolute;
|
||||
width: calc(100% - 16px);
|
||||
left: 0;
|
||||
top: 0;
|
||||
left: 11px;
|
||||
width: calc(100% - 11px);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -273,6 +271,13 @@ video::-webkit-media-controls-enclosure {
|
||||
cursor: pointer;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__loaded {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 0;
|
||||
width: calc(100% - 12px);
|
||||
}
|
||||
}
|
||||
|
||||
input[type=range] {
|
||||
|
11
src/types.d.ts
vendored
11
src/types.d.ts
vendored
@ -89,6 +89,17 @@ export type AccountPassword = {
|
||||
secure_random: Uint8Array,
|
||||
};
|
||||
|
||||
export type storageFileType = 'storage.fileUnknown' | 'storage.filePartial' | 'storage.fileJpeg' |
|
||||
'storage.fileGif' | 'storage.filePng' | 'storage.filePdf' | 'storage.fileMp3' | 'storage.fileMov' |
|
||||
'storage.fileMp4' | 'storage.fileWebp';
|
||||
|
||||
export type UploadFile = {
|
||||
_: 'upload.file',
|
||||
type: storageFileType,
|
||||
mtime: number,
|
||||
bytes: Uint8Array
|
||||
};
|
||||
|
||||
export type FileLocation = {
|
||||
_: 'fileLocationToBeDeprecated',
|
||||
volume_id: string,
|
||||
|
@ -73,8 +73,7 @@ module.exports = merge(common, {
|
||||
|| file.includes('pako')
|
||||
|| file.includes('Worker.min.js')
|
||||
|| file.includes('recorder.min.js')
|
||||
|| file.includes('.hbs')
|
||||
|| file.includes('mp4box')) return;
|
||||
|| file.includes('.hbs')) return;
|
||||
|
||||
let p = path.resolve(buildDir + file);
|
||||
if(!newlyCreatedAssets[file] && ['.gz', '.js'].find(ext => file.endsWith(ext)) !== undefined) {
|
||||
|
Loading…
Reference in New Issue
Block a user