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": {
|
"ms": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
@ -50,7 +50,6 @@
|
|||||||
"lottie-web": "^5.6.10",
|
"lottie-web": "^5.6.10",
|
||||||
"media-query-plugin": "^1.3.1",
|
"media-query-plugin": "^1.3.1",
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"mp4box": "^0.3.20",
|
|
||||||
"node-sass": "^4.14.1",
|
"node-sass": "^4.14.1",
|
||||||
"npm": "^6.14.5",
|
"npm": "^6.14.5",
|
||||||
"on-build-webpack": "^0.1.0",
|
"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";
|
import { RLottiePlayer } from "../lib/lottieLoader";
|
||||||
|
|
||||||
export interface AnimationItem {
|
export interface AnimationItem {
|
||||||
@ -18,6 +18,8 @@ export class AnimationIntersector {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.observer = new IntersectionObserver((entries) => {
|
this.observer = new IntersectionObserver((entries) => {
|
||||||
|
if($rootScope.idle.isIDLE) return;
|
||||||
|
|
||||||
for(const entry of entries) {
|
for(const entry of entries) {
|
||||||
const target = entry.target;
|
const target = entry.target;
|
||||||
|
|
||||||
@ -72,6 +74,8 @@ export class AnimationIntersector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public checkAnimations(blurred?: boolean, group?: string, destroy = false) {
|
public checkAnimations(blurred?: boolean, group?: string, destroy = false) {
|
||||||
|
if($rootScope.idle.isIDLE) return;
|
||||||
|
|
||||||
const groups = group /* && false */ ? [group] : Object.keys(this.byGroups);
|
const groups = group /* && false */ ? [group] : Object.keys(this.byGroups);
|
||||||
|
|
||||||
if(group && !this.byGroups[group]) {
|
if(group && !this.byGroups[group]) {
|
||||||
|
@ -48,11 +48,8 @@ class AppAudio {
|
|||||||
|
|
||||||
audio.addEventListener('pause', this.onPause);
|
audio.addEventListener('pause', this.onPause);
|
||||||
audio.addEventListener('ended', this.onEnded);
|
audio.addEventListener('ended', this.onEnded);
|
||||||
|
|
||||||
appDocsManager.downloadDoc(doc.id).then(() => {
|
const onError = (e: Event) => {
|
||||||
this.container.append(audio);
|
|
||||||
source.src = doc.url;
|
|
||||||
}, () => {
|
|
||||||
if(this.nextMid == mid) {
|
if(this.nextMid == mid) {
|
||||||
this.loadSiblingsAudio(doc.type as 'voice' | 'audio', mid).then(() => {
|
this.loadSiblingsAudio(doc.type as 'voice' | 'audio', mid).then(() => {
|
||||||
if(this.nextMid && this.audios[this.nextMid]) {
|
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;
|
return this.audios[mid] = audio;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import appDocsManager from "../lib/appManagers/appDocsManager";
|
import appDocsManager from "../lib/appManagers/appDocsManager";
|
||||||
import { RichTextProcessor } from "../lib/richtextprocessor";
|
import { RichTextProcessor } from "../lib/richtextprocessor";
|
||||||
import { formatDate } from "./wrappers";
|
import { formatDate } from "./wrappers";
|
||||||
import ProgressivePreloader from "./preloader";
|
import ProgressivePreloader from "./preloader_new";
|
||||||
import { CancellablePromise } from "../lib/polyfill";
|
|
||||||
import { MediaProgressLine } from "../lib/mediaPlayer";
|
import { MediaProgressLine } from "../lib/mediaPlayer";
|
||||||
import appAudio from "./appAudio";
|
import appAudio from "./appAudio";
|
||||||
import { MTDocument } from "../types";
|
import { MTDocument } from "../types";
|
||||||
import { mediaSizes } from "../lib/config";
|
import { mediaSizes } from "../lib/config";
|
||||||
|
import { Download } from "../lib/appManagers/appDownloadManager";
|
||||||
|
|
||||||
// https://github.com/LonamiWebs/Telethon/blob/4393ec0b83d511b6a20d8a20334138730f084375/telethon/utils.py#L1285
|
// https://github.com/LonamiWebs/Telethon/blob/4393ec0b83d511b6a20d8a20334138730f084375/telethon/utils.py#L1285
|
||||||
export function decodeWaveform(waveform: Uint8Array | number[]) {
|
export function decodeWaveform(waveform: Uint8Array | number[]) {
|
||||||
@ -233,7 +233,7 @@ function wrapAudio(doc: MTDocument, audioEl: AudioElement) {
|
|||||||
const subtitleDiv = audioEl.querySelector('.audio-subtitle') as HTMLDivElement;
|
const subtitleDiv = audioEl.querySelector('.audio-subtitle') as HTMLDivElement;
|
||||||
let launched = false;
|
let launched = false;
|
||||||
|
|
||||||
let progressLine = new MediaProgressLine(audioEl.audio);
|
let progressLine = new MediaProgressLine(audioEl.audio, doc.supportsStreaming);
|
||||||
|
|
||||||
audioEl.addAudioListener('ended', () => {
|
audioEl.addAudioListener('ended', () => {
|
||||||
audioEl.classList.remove('audio-show-progress');
|
audioEl.classList.remove('audio-show-progress');
|
||||||
@ -295,19 +295,23 @@ export default class AudioElement extends HTMLElement {
|
|||||||
|
|
||||||
const durationStr = String(doc.duration | 0).toHHMMSS(true);
|
const durationStr = String(doc.duration | 0).toHHMMSS(true);
|
||||||
|
|
||||||
this.innerHTML = `
|
this.innerHTML = `<div class="audio-toggle audio-ico tgico-largeplay"></div>`;
|
||||||
<div class="audio-toggle audio-ico tgico-largeplay"></div>
|
|
||||||
<div class="audio-download">${uploading ? '' : '<div class="tgico-download"></div>'}</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 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;
|
const audioTimeDiv = this.querySelector('.audio-time') as HTMLDivElement;
|
||||||
audioTimeDiv.innerHTML = durationStr;
|
audioTimeDiv.innerHTML = durationStr;
|
||||||
|
|
||||||
let preloader: ProgressivePreloader = this.preloader;
|
|
||||||
let promise: CancellablePromise<Blob>;
|
|
||||||
|
|
||||||
const onLoad = () => {
|
const onLoad = () => {
|
||||||
const audio = this.audio = appAudio.addAudio(doc, mid);
|
const audio = this.audio = appAudio.addAudio(doc, mid);
|
||||||
|
|
||||||
@ -351,35 +355,64 @@ export default class AudioElement extends HTMLElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if(!uploading) {
|
if(!uploading) {
|
||||||
const onClick = () => {
|
let preloader: ProgressivePreloader = this.preloader;
|
||||||
if(!promise) {
|
|
||||||
if(!preloader) {
|
if(doc.type == 'voice') {
|
||||||
preloader = new ProgressivePreloader(null, true);
|
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, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
promise = appDocsManager.downloadDoc(doc.id);
|
|
||||||
preloader.attach(downloadDiv, true, promise);
|
|
||||||
|
|
||||||
promise.then(() => {
|
|
||||||
preloader = null;
|
|
||||||
downloadDiv.classList.remove('downloading');
|
|
||||||
downloadDiv.remove();
|
|
||||||
this.removeEventListener('click', onClick);
|
|
||||||
onLoad();
|
|
||||||
});
|
|
||||||
|
|
||||||
downloadDiv.classList.add('downloading');
|
|
||||||
} else {
|
|
||||||
downloadDiv.classList.remove('downloading');
|
|
||||||
promise.cancel();
|
|
||||||
promise = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
this.addEventListener('click', onClick);
|
preloader.attach(downloadDiv);
|
||||||
this.click();
|
this.append(downloadDiv);
|
||||||
|
|
||||||
|
new Promise((resolve) => {
|
||||||
|
if(this.audio.readyState >= 2) resolve();
|
||||||
|
else this.addAudioListener('canplay', resolve);
|
||||||
|
}).then(() => {
|
||||||
|
downloadDiv.remove();
|
||||||
|
this.audio.play();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.addEventListener('click', r, {once: true});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.preloader.attach(this.querySelector('.audio-download'), false);
|
this.preloader.attach(downloadDiv, false);
|
||||||
//onLoad();
|
//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 appDocsManager from "../lib/appManagers/appDocsManager";
|
||||||
import { formatBytes, getEmojiToneIndex } from "../lib/utils";
|
import { formatBytes, getEmojiToneIndex } from "../lib/utils";
|
||||||
import ProgressivePreloader from './preloader';
|
import ProgressivePreloader from './preloader';
|
||||||
|
import ProgressivePreloaderNew from './preloader_new';
|
||||||
import LazyLoadQueue from './lazyLoadQueue';
|
import LazyLoadQueue from './lazyLoadQueue';
|
||||||
import VideoPlayer from '../lib/mediaPlayer';
|
import VideoPlayer from '../lib/mediaPlayer';
|
||||||
import { RichTextProcessor } from '../lib/richtextprocessor';
|
import { RichTextProcessor } from '../lib/richtextprocessor';
|
||||||
@ -18,6 +19,7 @@ import { mediaSizes } from '../lib/config';
|
|||||||
import { MTDocument, MTPhotoSize } from '../types';
|
import { MTDocument, MTPhotoSize } from '../types';
|
||||||
import animationIntersector from './animationIntersector';
|
import animationIntersector from './animationIntersector';
|
||||||
import AudioElement from './audio';
|
import AudioElement from './audio';
|
||||||
|
import { Download } from '../lib/appManagers/appDownloadManager';
|
||||||
|
|
||||||
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue}: {
|
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue}: {
|
||||||
doc: MTDocument,
|
doc: MTDocument,
|
||||||
@ -93,14 +95,14 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
|
|||||||
if(message.media.preloader) { // means upload
|
if(message.media.preloader) { // means upload
|
||||||
(message.media.preloader as ProgressivePreloader).attach(container, undefined, undefined, false);
|
(message.media.preloader as ProgressivePreloader).attach(container, undefined, undefined, false);
|
||||||
} else if(!doc.downloaded) {
|
} else if(!doc.downloaded) {
|
||||||
const promise = appDocsManager.downloadDoc(doc.id);
|
/* const promise = appDocsManager.downloadDoc(doc.id);
|
||||||
|
|
||||||
//if(!doc.supportsStreaming) {
|
//if(!doc.supportsStreaming) {
|
||||||
const preloader = new ProgressivePreloader(container, true);
|
const preloader = new ProgressivePreloader(container, true);
|
||||||
preloader.attach(container, true, promise, false);
|
preloader.attach(container, true, promise, false);
|
||||||
//}
|
//}
|
||||||
|
|
||||||
await promise;
|
await promise; */
|
||||||
}
|
}
|
||||||
|
|
||||||
if(middleware && !middleware()) {
|
if(middleware && !middleware()) {
|
||||||
@ -215,34 +217,35 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals
|
|||||||
|
|
||||||
if(!uploading) {
|
if(!uploading) {
|
||||||
let downloadDiv = docDiv.querySelector('.document-download') as HTMLDivElement;
|
let downloadDiv = docDiv.querySelector('.document-download') as HTMLDivElement;
|
||||||
let preloader: ProgressivePreloader;
|
let preloader: ProgressivePreloaderNew;
|
||||||
let promise: CancellablePromise<Blob>;
|
let download: Download;
|
||||||
|
|
||||||
docDiv.addEventListener('click', () => {
|
docDiv.addEventListener('click', () => {
|
||||||
if(!promise) {
|
if(!download) {
|
||||||
if(downloadDiv.classList.contains('downloading')) {
|
if(downloadDiv.classList.contains('downloading')) {
|
||||||
return; // means not ready yet
|
return; // means not ready yet
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!preloader) {
|
if(!preloader) {
|
||||||
preloader = new ProgressivePreloader(null, true);
|
preloader = new ProgressivePreloaderNew(null, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
download = appDocsManager.saveDocFile(doc);
|
||||||
|
preloader.attach(downloadDiv, true, appDocsManager.getInputFileName(doc));
|
||||||
|
|
||||||
appDocsManager.saveDocFile(doc.id).then(res => {
|
download.promise.then(() => {
|
||||||
promise = res.promise;
|
downloadDiv.remove();
|
||||||
|
}).catch(err => {
|
||||||
preloader.attach(downloadDiv, true, promise);
|
if(err.name === 'AbortError') {
|
||||||
|
download = null;
|
||||||
promise.then(() => {
|
}
|
||||||
downloadDiv.classList.remove('downloading');
|
}).finally(() => {
|
||||||
downloadDiv.remove();
|
downloadDiv.classList.remove('downloading');
|
||||||
});
|
});
|
||||||
})
|
|
||||||
|
|
||||||
downloadDiv.classList.add('downloading');
|
downloadDiv.classList.add('downloading');
|
||||||
} else {
|
} else {
|
||||||
downloadDiv.classList.remove('downloading');
|
download.controller.abort();
|
||||||
promise = null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -480,7 +483,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
|
|||||||
}
|
}
|
||||||
|
|
||||||
let downloaded = doc.downloaded;
|
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);
|
//console.log('loaded sticker:', doc, div);
|
||||||
if(middleware && !middleware()) return;
|
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);
|
dom = this.getDialogDom(dialog.peerID);
|
||||||
|
|
||||||
if(!dom) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1034,7 +1034,7 @@ export class AppDialogsManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(!dom) {
|
if(!dom) {
|
||||||
this.log.error('setUnreadMessages no dom!', dialog);
|
//this.log.error('setUnreadMessages no dom!', dialog);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
import FileManager from '../filemanager';
|
|
||||||
import {RichTextProcessor} from '../richtextprocessor';
|
import {RichTextProcessor} from '../richtextprocessor';
|
||||||
import { CancellablePromise, deferredPromise } from '../polyfill';
|
import { CancellablePromise, deferredPromise } from '../polyfill';
|
||||||
import { isObject, getFileURL } from '../utils';
|
import { isObject, getFileURL } from '../utils';
|
||||||
import opusDecodeController from '../opusDecodeController';
|
import opusDecodeController from '../opusDecodeController';
|
||||||
import { MTDocument, inputDocumentFileLocation } from '../../types';
|
import { MTDocument, inputDocumentFileLocation } from '../../types';
|
||||||
|
import { getFileNameByLocation } from '../bin_utils';
|
||||||
|
import appDownloadManager, { Download } from './appDownloadManager';
|
||||||
|
|
||||||
class AppDocsManager {
|
class AppDocsManager {
|
||||||
private docs: {[docID: string]: MTDocument} = {};
|
private docs: {[docID: string]: MTDocument} = {};
|
||||||
private thumbs: {[docIDAndSize: string]: Promise<string>} = {};
|
private thumbs: {[docIDAndSize: string]: Promise<string>} = {};
|
||||||
private downloadPromises: {[docID: string]: CancellablePromise<Blob>} = {};
|
private downloadPromises: {[docID: string]: CancellablePromise<Blob>} = {};
|
||||||
|
|
||||||
public saveDoc(apiDoc: MTDocument, context?: any) {
|
public saveDoc(apiDoc: MTDocument, context?: any) {
|
||||||
//console.log('saveDoc', apiDoc, this.docs[apiDoc.id]);
|
//console.log('saveDoc', apiDoc, this.docs[apiDoc.id]);
|
||||||
if(this.docs[apiDoc.id]) {
|
if(this.docs[apiDoc.id]) {
|
||||||
@ -47,13 +48,17 @@ class AppDocsManager {
|
|||||||
apiDoc.audioTitle = attribute.title;
|
apiDoc.audioTitle = attribute.title;
|
||||||
apiDoc.audioPerformer = attribute.performer;
|
apiDoc.audioPerformer = attribute.performer;
|
||||||
apiDoc.type = attribute.pFlags.voice && apiDoc.mime_type == "audio/ogg" ? 'voice' : 'audio';
|
apiDoc.type = attribute.pFlags.voice && apiDoc.mime_type == "audio/ogg" ? 'voice' : 'audio';
|
||||||
|
|
||||||
|
if(apiDoc.type == 'audio') {
|
||||||
|
apiDoc.supportsStreaming = true;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'documentAttributeVideo':
|
case 'documentAttributeVideo':
|
||||||
apiDoc.duration = attribute.duration;
|
apiDoc.duration = attribute.duration;
|
||||||
apiDoc.w = attribute.w;
|
apiDoc.w = attribute.w;
|
||||||
apiDoc.h = attribute.h;
|
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) {
|
if(apiDoc.thumbs && attribute.pFlags.round_message) {
|
||||||
apiDoc.type = 'round';
|
apiDoc.type = 'round';
|
||||||
} else /* if(apiDoc.thumbs) */ {
|
} else /* if(apiDoc.thumbs) */ {
|
||||||
@ -134,6 +139,10 @@ class AppDocsManager {
|
|||||||
apiDoc.size = 0;
|
apiDoc.size = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!apiDoc.url) {
|
||||||
|
apiDoc.url = this.getFileURLByDoc(apiDoc);
|
||||||
|
}
|
||||||
|
|
||||||
return apiDoc;
|
return apiDoc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,9 +190,21 @@ class AppDocsManager {
|
|||||||
return 't_' + (doc.type || 'file') + doc.id + fileExt;
|
return 't_' + (doc.type || 'file') + doc.id + fileExt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getFileURLByDoc(doc: MTDocument) {
|
public getFileURLByDoc(doc: MTDocument, download = false) {
|
||||||
const inputFileLocation = this.getInputByID(doc);
|
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> {
|
public downloadDoc(docID: string | MTDocument, toFileEntry?: any): CancellablePromise<Blob> {
|
||||||
@ -252,6 +273,56 @@ class AppDocsManager {
|
|||||||
return this.downloadPromises[doc.id] = deferred;
|
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) {
|
public downloadDocThumb(docID: any, thumbSize: string) {
|
||||||
let doc = this.getDoc(docID);
|
let doc = this.getDoc(docID);
|
||||||
|
|
||||||
@ -273,33 +344,12 @@ class AppDocsManager {
|
|||||||
public hasDownloadedThumb(docID: string, thumbSize: string) {
|
public hasDownloadedThumb(docID: string, thumbSize: string) {
|
||||||
return !!this.thumbs[docID + '-' + thumbSize];
|
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] || '';
|
|
||||||
|
|
||||||
try {
|
public saveDocFile(doc: MTDocument) {
|
||||||
let writer = FileManager.chooseSaveFile(fileName, ext, doc.mime_type, doc.size);
|
const url = this.getFileURLByDoc(doc, true);
|
||||||
await writer.ready;
|
const fileName = this.getInputFileName(doc);
|
||||||
|
|
||||||
let promise = this.downloadDoc(docID, writer);
|
return appDownloadManager.downloadToDisc(fileName, url);
|
||||||
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};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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', () => {
|
window.addEventListener('blur', () => {
|
||||||
animationIntersector.checkAnimations(true);
|
animationIntersector.checkAnimations(true);
|
||||||
|
|
||||||
this.offline = true;
|
this.offline = $rootScope.idle.isIDLE = true;
|
||||||
this.updateStatus();
|
this.updateStatus();
|
||||||
clearInterval(this.updateStatusInterval);
|
clearInterval(this.updateStatusInterval);
|
||||||
|
|
||||||
window.addEventListener('focus', () => {
|
window.addEventListener('focus', () => {
|
||||||
animationIntersector.checkAnimations(false);
|
this.offline = $rootScope.idle.isIDLE = false;
|
||||||
|
|
||||||
this.offline = false;
|
|
||||||
this.updateStatus();
|
this.updateStatus();
|
||||||
this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3);
|
this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3);
|
||||||
|
|
||||||
|
// в обратном порядке
|
||||||
|
animationIntersector.checkAnimations(false);
|
||||||
}, {once: true});
|
}, {once: true});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@ import AvatarElement from "../../components/avatar";
|
|||||||
import LazyLoadQueue from "../../components/lazyLoadQueue";
|
import LazyLoadQueue from "../../components/lazyLoadQueue";
|
||||||
import appForward from "../../components/appForward";
|
import appForward from "../../components/appForward";
|
||||||
import { isSafari, mediaSizes } from "../config";
|
import { isSafari, mediaSizes } from "../config";
|
||||||
import MP4Source from "../MP4Source";
|
|
||||||
|
|
||||||
export class AppMediaViewer {
|
export class AppMediaViewer {
|
||||||
public wholeDiv = document.querySelector('.media-viewer-whole') as HTMLDivElement;
|
public wholeDiv = document.querySelector('.media-viewer-whole') as HTMLDivElement;
|
||||||
@ -806,7 +805,7 @@ export class AppMediaViewer {
|
|||||||
video.append(source);
|
video.append(source);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createPlayer = (streamable = false) => {
|
const createPlayer = () => {
|
||||||
if(media.type != 'gif') {
|
if(media.type != 'gif') {
|
||||||
video.dataset.ckin = 'default';
|
video.dataset.ckin = 'default';
|
||||||
video.dataset.overlay = '1';
|
video.dataset.overlay = '1';
|
||||||
@ -815,7 +814,7 @@ export class AppMediaViewer {
|
|||||||
div.append(video);
|
div.append(video);
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = new VideoPlayer(video, true, streamable);
|
const player = new VideoPlayer(video, true, media.supportsStreaming);
|
||||||
return player;
|
return player;
|
||||||
/* player.wrapper.parentElement.append(video);
|
/* player.wrapper.parentElement.append(video);
|
||||||
mover.append(player.wrapper); */
|
mover.append(player.wrapper); */
|
||||||
@ -827,61 +826,20 @@ export class AppMediaViewer {
|
|||||||
if(!source.src || (media.url && media.url != source.src)) {
|
if(!source.src || (media.url && media.url != source.src)) {
|
||||||
const load = () => {
|
const load = () => {
|
||||||
const promise = appDocsManager.downloadDoc(media.id);
|
const promise = appDocsManager.downloadDoc(media.id);
|
||||||
|
|
||||||
const streamable = media.supportsStreaming && !media.url;
|
|
||||||
//if(!streamable) {
|
//if(!streamable) {
|
||||||
this.preloader.attach(mover, true, promise);
|
this.preloader.attach(mover, true, promise);
|
||||||
//}
|
//}
|
||||||
|
|
||||||
let player: VideoPlayer;
|
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;
|
promise.then(async() => {
|
||||||
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) => {
|
|
||||||
if(this.currentMessageID != message.mid) {
|
if(this.currentMessageID != message.mid) {
|
||||||
this.log.warn('media viewer changed video');
|
this.log.warn('media viewer changed video');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isStream = mp4Source instanceof MP4Source;
|
const url = media.url;
|
||||||
if(isStream) {
|
|
||||||
promise.notify = promiseNotify;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = isStream ? mp4Source.getURL() : media.url;
|
|
||||||
if(target instanceof SVGSVGElement && (video.parentElement || !isSafari)) { // if video exists
|
if(target instanceof SVGSVGElement && (video.parentElement || !isSafari)) { // if video exists
|
||||||
if(!video.parentElement) {
|
if(!video.parentElement) {
|
||||||
div.firstElementChild.lastElementChild.append(video);
|
div.firstElementChild.lastElementChild.append(video);
|
||||||
@ -914,16 +872,7 @@ export class AppMediaViewer {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
player = createPlayer(streamable);
|
player = createPlayer();
|
||||||
if(player && mp4Source instanceof MP4Source) {
|
|
||||||
player.progress.onSeek = (time) => {
|
|
||||||
//this.log('seek', time);
|
|
||||||
offset = mp4Source.seek(time);
|
|
||||||
setLoadProgress();
|
|
||||||
};
|
|
||||||
|
|
||||||
this.log('lol');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import appUsersManager from "./appUsersManager";
|
import appUsersManager from "./appUsersManager";
|
||||||
import { calcImageInBox, isObject, getFileURL } from "../utils";
|
import { calcImageInBox, isObject, getFileURL } from "../utils";
|
||||||
import fileManager from '../filemanager';
|
import { bytesFromHex, getFileNameByLocation } from "../bin_utils";
|
||||||
import { bytesFromHex } from "../bin_utils";
|
|
||||||
//import apiManager from '../mtproto/apiManager';
|
//import apiManager from '../mtproto/apiManager';
|
||||||
import apiManager from '../mtproto/mtprotoworker';
|
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 = {
|
export type MTPhoto = {
|
||||||
_: 'photo' | 'photoEmpty' | string,
|
_: 'photo' | 'photoEmpty' | string,
|
||||||
@ -58,38 +58,6 @@ export class AppPhotosManager {
|
|||||||
if(!photo.id) {
|
if(!photo.id) {
|
||||||
console.warn('no apiPhoto.id', photo);
|
console.warn('no apiPhoto.id', photo);
|
||||||
} else this.photos[photo.id] = photo as any;
|
} 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;
|
return photo;
|
||||||
}
|
}
|
||||||
@ -326,52 +294,22 @@ export class AppPhotosManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public downloadPhoto(photoID: string) {
|
public downloadPhoto(photoID: string) {
|
||||||
var photo = this.photos[photoID];
|
const photo = this.photos[photoID];
|
||||||
var ext = 'jpg';
|
const fullWidth = this.windowW;
|
||||||
var mimeType = 'image/jpeg';
|
const fullHeight = this.windowH;
|
||||||
var fileName = 'photo' + photoID + '.' + ext;
|
const fullPhotoSize = this.choosePhotoSize(photo, fullWidth, fullHeight);
|
||||||
var fullWidth = this.windowW;
|
const location: inputDocumentFileLocation | inputPhotoFileLocation = {
|
||||||
var fullHeight = this.windowH;
|
|
||||||
var fullPhotoSize = this.choosePhotoSize(photo, fullWidth, fullHeight);
|
|
||||||
var inputFileLocation: inputDocumentFileLocation | inputPhotoFileLocation = {
|
|
||||||
// @ts-ignore
|
|
||||||
_: photo._ == 'document' ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation',
|
_: photo._ == 'document' ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation',
|
||||||
id: photo.id,
|
id: photo.id,
|
||||||
access_hash: photo.access_hash,
|
access_hash: photo.access_hash,
|
||||||
file_reference: photo.file_reference,
|
file_reference: photo.file_reference,
|
||||||
thumb_size: fullPhotoSize.type
|
thumb_size: fullPhotoSize.type
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const url = getFileURL('download', {dcID: photo.dc_id, location, size: fullPhotoSize.size, fileName: 'photo' + photo.id + '.jpg'});
|
||||||
|
const fileName = getFileNameByLocation(location);
|
||||||
|
|
||||||
try { // photo.dc_id, location, photoSize.size
|
appDownloadManager.downloadToDisc(fileName, url);
|
||||||
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);
|
|
||||||
|
|
||||||
/* 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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,5 @@
|
|||||||
import {blobConstruct} from './bin_utils';
|
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 {
|
class FileManager {
|
||||||
public blobSupported = true;
|
public blobSupported = true;
|
||||||
|
|
||||||
@ -21,21 +14,7 @@ class FileManager {
|
|||||||
public isAvailable() {
|
public isAvailable() {
|
||||||
return this.blobSupported;
|
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) {
|
public copy(fromFileEntry: any, toFileEntry: any) {
|
||||||
return this.write(toFileEntry, fromFileEntry).then(() => {
|
return this.write(toFileEntry, fromFileEntry).then(() => {
|
||||||
console.log('copy success');
|
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> {
|
public write(fileWriter: ReturnType<FileManager['getFakeFileWriter']>, bytes: Uint8Array | Blob | {file: any}): Promise<void> {
|
||||||
if('file' in bytes) {
|
if('file' in bytes) {
|
||||||
return bytes.file((file: any) => {
|
return bytes.file((file: any) => {
|
||||||
@ -97,23 +50,10 @@ class FileManager {
|
|||||||
fileReader.readAsArrayBuffer(bytes);
|
fileReader.readAsArrayBuffer(bytes);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
//var blob = blobConstruct([bytesToArrayBuffer(bytes)]);
|
|
||||||
//return fileWriter.write(blob);
|
|
||||||
return fileWriter.write(bytes);
|
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>) {
|
public getFakeFileWriter(mimeType: string, saveFileCallback: (blob: Blob) => Promise<Blob>) {
|
||||||
let blobParts: Array<Uint8Array> = [];
|
let blobParts: Array<Uint8Array> = [];
|
||||||
const fakeFileWriter = {
|
const fakeFileWriter = {
|
||||||
@ -139,70 +79,6 @@ class FileManager {
|
|||||||
|
|
||||||
return fakeFileWriter;
|
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();
|
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;
|
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 = document.createElement('div');
|
||||||
this.container.classList.add('media-progress');
|
this.container.classList.add('media-progress');
|
||||||
|
|
||||||
@ -22,6 +22,7 @@ export class MediaProgressLine {
|
|||||||
this.filledLoad = document.createElement('div');
|
this.filledLoad = document.createElement('div');
|
||||||
this.filledLoad.classList.add('media-progress__filled', 'media-progress__loaded');
|
this.filledLoad.classList.add('media-progress__filled', 'media-progress__loaded');
|
||||||
this.container.append(this.filledLoad);
|
this.container.append(this.filledLoad);
|
||||||
|
//this.setLoadProgress();
|
||||||
}
|
}
|
||||||
|
|
||||||
let seek = this.seek = document.createElement('input');
|
let seek = this.seek = document.createElement('input');
|
||||||
@ -62,6 +63,10 @@ export class MediaProgressLine {
|
|||||||
window.cancelAnimationFrame(this.progressRAF);
|
window.cancelAnimationFrame(this.progressRAF);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(this.streamable) {
|
||||||
|
this.setLoadProgress();
|
||||||
|
}
|
||||||
|
|
||||||
this.progressRAF = window.requestAnimationFrame(r);
|
this.progressRAF = window.requestAnimationFrame(r);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -96,7 +101,29 @@ export class MediaProgressLine {
|
|||||||
this.mousedown = false;
|
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 + ')';
|
this.filledLoad.style.transform = 'scaleX(' + percents + ')';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,6 +147,7 @@ export class MediaProgressLine {
|
|||||||
private setListeners() {
|
private setListeners() {
|
||||||
this.media.addEventListener('ended', this.onEnded);
|
this.media.addEventListener('ended', this.onEnded);
|
||||||
this.media.addEventListener('play', this.onPlay);
|
this.media.addEventListener('play', this.onPlay);
|
||||||
|
this.streamable && this.media.addEventListener('progress', this.onProgress);
|
||||||
|
|
||||||
this.container.addEventListener('mousemove', this.onMouseMove);
|
this.container.addEventListener('mousemove', this.onMouseMove);
|
||||||
this.container.addEventListener('mousedown', this.onMouseDown);
|
this.container.addEventListener('mousedown', this.onMouseDown);
|
||||||
@ -143,6 +171,7 @@ export class MediaProgressLine {
|
|||||||
this.media.removeEventListener('loadeddata', this.onLoadedData);
|
this.media.removeEventListener('loadeddata', this.onLoadedData);
|
||||||
this.media.removeEventListener('ended', this.onEnded);
|
this.media.removeEventListener('ended', this.onEnded);
|
||||||
this.media.removeEventListener('play', this.onPlay);
|
this.media.removeEventListener('play', this.onPlay);
|
||||||
|
this.streamable && this.media.removeEventListener('progress', this.onProgress);
|
||||||
|
|
||||||
this.container.removeEventListener('mousemove', this.onMouseMove);
|
this.container.removeEventListener('mousemove', this.onMouseMove);
|
||||||
this.container.removeEventListener('mousedown', this.onMouseDown);
|
this.container.removeEventListener('mousedown', this.onMouseDown);
|
||||||
|
@ -6,7 +6,7 @@ import apiManager from "./apiManager";
|
|||||||
import { deferredPromise, CancellablePromise } from "../polyfill";
|
import { deferredPromise, CancellablePromise } from "../polyfill";
|
||||||
import appWebpManager from "../appManagers/appWebpManager";
|
import appWebpManager from "../appManagers/appWebpManager";
|
||||||
import { logger } from "../logger";
|
import { logger } from "../logger";
|
||||||
import { InputFileLocation, FileLocation } from "../../types";
|
import { InputFileLocation, FileLocation, UploadFile } from "../../types";
|
||||||
|
|
||||||
type Delayed = {
|
type Delayed = {
|
||||||
offset: number,
|
offset: number,
|
||||||
@ -14,18 +14,25 @@ type Delayed = {
|
|||||||
writeFileDeferred: CancellablePromise<unknown>
|
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 {
|
export class ApiFileManager {
|
||||||
public cachedDownloadPromises: {
|
public cachedDownloadPromises: {
|
||||||
[fileName: string]: Promise<Blob>
|
[fileName: string]: CancellablePromise<Blob>
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
public downloadPulls: {
|
public downloadPulls: {
|
||||||
[x: string]: Array<{
|
[x: string]: Array<{
|
||||||
cb: () => Promise<unknown>,
|
cb: () => Promise<UploadFile | void>,
|
||||||
deferred: {
|
deferred: {
|
||||||
resolve: (...args: any[]) => void,
|
resolve: (...args: any[]) => void,
|
||||||
reject: (...args: any[]) => void
|
reject: (...args: any[]) => void
|
||||||
@ -37,7 +44,9 @@ export class ApiFileManager {
|
|||||||
|
|
||||||
private log: ReturnType<typeof logger> = logger('AFM');
|
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) {
|
if(this.downloadPulls[dcID] === undefined) {
|
||||||
this.downloadPulls[dcID] = [];
|
this.downloadPulls[dcID] = [];
|
||||||
this.downloadActives[dcID] = 0;
|
this.downloadActives[dcID] = 0;
|
||||||
@ -45,9 +54,9 @@ export class ApiFileManager {
|
|||||||
|
|
||||||
const downloadPull = this.downloadPulls[dcID];
|
const downloadPull = this.downloadPulls[dcID];
|
||||||
|
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise<UploadFile | void>((resolve, reject) => {
|
||||||
downloadPull.push({cb: cb, deferred: {resolve, reject}, activeDelta: activeDelta});
|
downloadPull.push({cb, deferred: {resolve, reject}, activeDelta});
|
||||||
})/* .catch(() => {}) */;
|
});
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.downloadCheck(dcID);
|
this.downloadCheck(dcID);
|
||||||
@ -60,7 +69,7 @@ export class ApiFileManager {
|
|||||||
const downloadPull = this.downloadPulls[dcID];
|
const downloadPull = this.downloadPulls[dcID];
|
||||||
//const downloadLimit = dcID == 'upload' ? 11 : 5;
|
//const downloadLimit = dcID == 'upload' ? 11 : 5;
|
||||||
//const downloadLimit = 24;
|
//const downloadLimit = 24;
|
||||||
const downloadLimit = dcID == 'upload' ? 11 : 24;
|
const downloadLimit = dcID == 'upload' ? 11 : 50;
|
||||||
|
|
||||||
if(this.downloadActives[dcID] >= downloadLimit || !downloadPull || !downloadPull.length) {
|
if(this.downloadActives[dcID] >= downloadLimit || !downloadPull || !downloadPull.length) {
|
||||||
return false;
|
return false;
|
||||||
@ -72,12 +81,12 @@ export class ApiFileManager {
|
|||||||
this.downloadActives[dcID] += activeDelta;
|
this.downloadActives[dcID] += activeDelta;
|
||||||
|
|
||||||
data.cb()
|
data.cb()
|
||||||
.then((result: any) => {
|
.then((result) => {
|
||||||
this.downloadActives[dcID] -= activeDelta;
|
this.downloadActives[dcID] -= activeDelta;
|
||||||
this.downloadCheck(dcID);
|
this.downloadCheck(dcID);
|
||||||
|
|
||||||
data.deferred.resolve(result);
|
data.deferred.resolve(result);
|
||||||
}, (error: any) => {
|
}, (error: Error) => {
|
||||||
if(error) {
|
if(error) {
|
||||||
this.log.error('downloadCheck error:', error);
|
this.log.error('downloadCheck error:', error);
|
||||||
}
|
}
|
||||||
@ -93,16 +102,48 @@ export class ApiFileManager {
|
|||||||
return cacheStorage;
|
return cacheStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
public downloadFile(options: {
|
public cancelDownload(fileName: string) {
|
||||||
dcID: number,
|
const promise = this.cachedDownloadPromises[fileName];
|
||||||
location: InputFileLocation | FileLocation,
|
if(promise) {
|
||||||
size: number,
|
promise.cancel();
|
||||||
mimeType?: string,
|
return true;
|
||||||
toFileEntry?: any,
|
}
|
||||||
limitPart?: number,
|
|
||||||
stickerType?: number,
|
return false;
|
||||||
processPart?: (bytes: Uint8Array, offset: number, queue: Delayed[]) => Promise<any>
|
}
|
||||||
}): CancellablePromise<Blob> {
|
|
||||||
|
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()) {
|
if(!FileManager.isAvailable()) {
|
||||||
return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'});
|
return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'});
|
||||||
}
|
}
|
||||||
@ -112,7 +153,7 @@ export class ApiFileManager {
|
|||||||
|
|
||||||
let processSticker = false;
|
let processSticker = false;
|
||||||
if(options.stickerType == 1 && !appWebpManager.isSupported()) {
|
if(options.stickerType == 1 && !appWebpManager.isSupported()) {
|
||||||
if(options.toFileEntry || size > 524288) {
|
if(size > 524288) {
|
||||||
delete options.stickerType;
|
delete options.stickerType;
|
||||||
} else {
|
} else {
|
||||||
processSticker = true;
|
processSticker = true;
|
||||||
@ -122,16 +163,18 @@ export class ApiFileManager {
|
|||||||
|
|
||||||
// this.log('Dload file', dcID, location, size)
|
// this.log('Dload file', dcID, location, size)
|
||||||
const fileName = getFileNameByLocation(location);
|
const fileName = getFileNameByLocation(location);
|
||||||
const toFileEntry = options.toFileEntry || null;
|
|
||||||
const cachedPromise = this.cachedDownloadPromises[fileName];
|
const cachedPromise = this.cachedDownloadPromises[fileName];
|
||||||
const fileStorage = this.getFileStorage();
|
const fileStorage = this.getFileStorage();
|
||||||
|
|
||||||
//this.log('downloadFile', fileName, fileName.length, location, arguments);
|
//this.log('downloadFile', fileName, fileName.length, location, arguments);
|
||||||
|
|
||||||
if(cachedPromise) {
|
if(cachedPromise) {
|
||||||
if(toFileEntry) {
|
if(options.processPart) {
|
||||||
return cachedPromise.then((blob: any) => {
|
return cachedPromise.then((blob) => {
|
||||||
return FileManager.copy(blob, toFileEntry);
|
return this.convertBlobToBytes(blob).then(bytes => {
|
||||||
|
options.processPart(bytes)
|
||||||
|
return blob;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,30 +224,28 @@ export class ApiFileManager {
|
|||||||
throw false;
|
throw false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(toFileEntry) {
|
if(options.processPart) {
|
||||||
FileManager.copy(blob, toFileEntry).then(deferred.resolve, errorHandler);
|
//FileManager.copy(blob, toFileEntry).then(deferred.resolve, errorHandler);
|
||||||
} else {
|
await this.convertBlobToBytes(blob).then(bytes => {
|
||||||
deferred.resolve(blob);
|
options.processPart(bytes);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deferred.resolve(blob);
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
//this.log('not cached', fileName);
|
//this.log('not cached', fileName);
|
||||||
//var fileWriterPromise = toFileEntry ? FileManager.getFileWriter(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType);
|
const fileWriterPromise = fileStorage.getFileWriter(fileName, mimeType);
|
||||||
const fileWriterPromise = toFileEntry ? Promise.resolve(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType);
|
|
||||||
|
|
||||||
fileWriterPromise.then((fileWriter: any) => {
|
fileWriterPromise.then((fileWriter) => {
|
||||||
cacheFileWriter = fileWriter;
|
cacheFileWriter = fileWriter;
|
||||||
const limit = options.limitPart || 524288;
|
const limit = options.limitPart || this.getLimitPart(size);
|
||||||
let offset: number;
|
let offset: number;
|
||||||
let startOffset = 0;
|
let startOffset = 0;
|
||||||
let writeFilePromise: CancellablePromise<unknown> = Promise.resolve(),
|
let writeFilePromise: CancellablePromise<unknown> = Promise.resolve(),
|
||||||
writeFileDeferred: CancellablePromise<unknown>;
|
writeFileDeferred: CancellablePromise<unknown>;
|
||||||
const maxRequests = options.processPart ? 5 : 5;
|
const maxRequests = options.processPart ? 5 : 10;
|
||||||
|
|
||||||
if(!size) {
|
/* if(fileWriter.length) {
|
||||||
size = limit;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(fileWriter.length) {
|
|
||||||
startOffset = fileWriter.length;
|
startOffset = fileWriter.length;
|
||||||
|
|
||||||
if(startOffset >= size) {
|
if(startOffset >= size) {
|
||||||
@ -221,7 +262,7 @@ export class ApiFileManager {
|
|||||||
deferred.notify({done: startOffset, total: size});
|
deferred.notify({done: startOffset, total: size});
|
||||||
|
|
||||||
/////this.log('deferred notify 1:', {done: startOffset, total: size});
|
/////this.log('deferred notify 1:', {done: startOffset, total: size});
|
||||||
}
|
} */
|
||||||
|
|
||||||
const processDownloaded = async(bytes: Uint8Array, offset: number) => {
|
const processDownloaded = async(bytes: Uint8Array, offset: number) => {
|
||||||
if(options.processPart) {
|
if(options.processPart) {
|
||||||
@ -236,18 +277,20 @@ export class ApiFileManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const delayed: Delayed[] = [];
|
const delayed: Delayed[] = [];
|
||||||
for(offset = startOffset; offset < size; offset += limit) {
|
offset = startOffset;
|
||||||
|
do {
|
||||||
|
////this.log('offset:', startOffset);
|
||||||
writeFileDeferred = deferredPromise<void>();
|
writeFileDeferred = deferredPromise<void>();
|
||||||
delayed.push({offset, writeFilePromise, writeFileDeferred});
|
delayed.push({offset, writeFilePromise, writeFileDeferred});
|
||||||
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];
|
const last = delayed.splice(delayed.length - 1, 1)[0];
|
||||||
delayed.splice(1, 0, last);
|
delayed.splice(1, 0, last);
|
||||||
}
|
} */
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
//deferred.queue = delayed;
|
//deferred.queue = delayed;
|
||||||
@ -258,21 +301,7 @@ export class ApiFileManager {
|
|||||||
|
|
||||||
const {offset, writeFilePromise, writeFileDeferred} = delayed.shift();
|
const {offset, writeFilePromise, writeFileDeferred} = delayed.shift();
|
||||||
try {
|
try {
|
||||||
const result: any = await this.downloadRequest(dcID, () => {
|
const result = await this.requestFilePart(dcID, location, offset, limit, checkCancel);
|
||||||
if(canceled) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiManager.invokeApi('upload.getFile', {
|
|
||||||
location,
|
|
||||||
offset,
|
|
||||||
limit
|
|
||||||
}, {
|
|
||||||
dcID,
|
|
||||||
fileDownload: true/* ,
|
|
||||||
singleInRequest: 'safari' in window */
|
|
||||||
});
|
|
||||||
}, 2);
|
|
||||||
|
|
||||||
if(delayed.length) {
|
if(delayed.length) {
|
||||||
superpuper();
|
superpuper();
|
||||||
@ -280,11 +309,10 @@ export class ApiFileManager {
|
|||||||
|
|
||||||
//////////////////////////////////////////
|
//////////////////////////////////////////
|
||||||
const processedResult = await processDownloaded(result.bytes, offset);
|
const processedResult = await processDownloaded(result.bytes, offset);
|
||||||
if(canceled) {
|
checkCancel();
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
done += limit;
|
//done += limit;
|
||||||
|
done += processedResult.byteLength;
|
||||||
const isFinal = offset + limit >= size;
|
const isFinal = offset + limit >= size;
|
||||||
//if(!isFinal) {
|
//if(!isFinal) {
|
||||||
////this.log('deferred notify 2:', {done: offset + limit, total: size}, deferred);
|
////this.log('deferred notify 2:', {done: offset + limit, total: size}, deferred);
|
||||||
@ -292,9 +320,7 @@ export class ApiFileManager {
|
|||||||
//}
|
//}
|
||||||
|
|
||||||
await writeFilePromise;
|
await writeFilePromise;
|
||||||
if(canceled) {
|
checkCancel();
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
await FileManager.write(fileWriter, processedResult);
|
await FileManager.write(fileWriter, processedResult);
|
||||||
writeFileDeferred.resolve();
|
writeFileDeferred.resolve();
|
||||||
@ -302,7 +328,7 @@ export class ApiFileManager {
|
|||||||
if(isFinal) {
|
if(isFinal) {
|
||||||
resolved = true;
|
resolved = true;
|
||||||
|
|
||||||
if(toFileEntry) {
|
if(options.processPart) {
|
||||||
deferred.resolve();
|
deferred.resolve();
|
||||||
} else {
|
} else {
|
||||||
deferred.resolve(fileWriter.finalize());
|
deferred.resolve(fileWriter.finalize());
|
||||||
@ -319,22 +345,21 @@ export class ApiFileManager {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const checkCancel = () => {
|
||||||
|
if(canceled) {
|
||||||
|
throw new Error('canceled');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
deferred.cancel = () => {
|
deferred.cancel = () => {
|
||||||
if(!canceled && !resolved) {
|
if(!canceled && !resolved) {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
delete this.cachedDownloadPromises[fileName];
|
delete this.cachedDownloadPromises[fileName];
|
||||||
errorHandler({type: 'DOWNLOAD_CANCELED'});
|
errorHandler({type: 'DOWNLOAD_CANCELED'});
|
||||||
if(toFileEntry) {
|
|
||||||
toFileEntry.abort();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//console.log(deferred, deferred.notify, deferred.cancel);
|
this.cachedDownloadPromises[fileName] = deferred;
|
||||||
|
|
||||||
if(!toFileEntry) {
|
|
||||||
this.cachedDownloadPromises[fileName] = deferred;
|
|
||||||
}
|
|
||||||
|
|
||||||
return deferred;
|
return deferred;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,11 @@ import apiManager from "./apiManager";
|
|||||||
import AppStorage from '../storage';
|
import AppStorage from '../storage';
|
||||||
import cryptoWorker from "../crypto/cryptoworker";
|
import cryptoWorker from "../crypto/cryptoworker";
|
||||||
import networkerFactory from "./networkerFactory";
|
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;
|
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');
|
* Broadcast Notification
|
||||||
//ctx.postMessage({update: {obj, bool}});
|
*/
|
||||||
//respond({update: {obj, bool}});
|
function notify(...args: any[]) {
|
||||||
|
|
||||||
ctx.clients.matchAll({ includeUncontrolled: false, type: 'window' }).then((listeners) => {
|
ctx.clients.matchAll({ includeUncontrolled: false, type: 'window' }).then((listeners) => {
|
||||||
if(!listeners.length) {
|
if(!listeners.length) {
|
||||||
//console.trace('no listeners?', self, listeners);
|
//console.trace('no listeners?', self, listeners);
|
||||||
@ -87,15 +90,20 @@ networkerFactory.setUpdatesProcessor((obj, bool) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
listeners.forEach(listener => {
|
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) => {
|
ctx.addEventListener('message', async(e) => {
|
||||||
const taskID = e.data.taskID;
|
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) {
|
if(e.data.useLs) {
|
||||||
AppStorage.finishTask(e.data.taskID, e.data.args);
|
AppStorage.finishTask(e.data.taskID, e.data.args);
|
||||||
@ -110,6 +118,7 @@ ctx.addEventListener('message', async(e) => {
|
|||||||
respond(e.source, {taskID: taskID, result: result});
|
respond(e.source, {taskID: taskID, result: result});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case 'cancelDownload':
|
||||||
case 'downloadFile': {
|
case 'downloadFile': {
|
||||||
/* // @ts-ignore
|
/* // @ts-ignore
|
||||||
return apiFileManager.downloadFile(...e.data.args); */
|
return apiFileManager.downloadFile(...e.data.args); */
|
||||||
@ -151,7 +160,7 @@ ctx.addEventListener('message', async(e) => {
|
|||||||
* Service Worker Installation
|
* Service Worker Installation
|
||||||
*/
|
*/
|
||||||
ctx.addEventListener('install', (event: ExtendableEvent) => {
|
ctx.addEventListener('install', (event: ExtendableEvent) => {
|
||||||
//console.log('service worker is installing');
|
log('installing');
|
||||||
|
|
||||||
/* initCache();
|
/* initCache();
|
||||||
|
|
||||||
@ -165,7 +174,7 @@ ctx.addEventListener('install', (event: ExtendableEvent) => {
|
|||||||
* Service Worker Activation
|
* Service Worker Activation
|
||||||
*/
|
*/
|
||||||
ctx.addEventListener('activate', (event) => {
|
ctx.addEventListener('activate', (event) => {
|
||||||
//console.log('service worker activating', ctx);
|
log('activating', ctx);
|
||||||
|
|
||||||
/* if (!ctx.cache) initCache();
|
/* if (!ctx.cache) initCache();
|
||||||
if (!ctx.network) initNetwork(); */
|
if (!ctx.network) initNetwork(); */
|
||||||
@ -184,28 +193,168 @@ function timeout(delay: number): Promise<Response> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx.addEventListener('error', (error) => {
|
||||||
|
log.error('error:', error);
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch requests
|
* Fetch requests
|
||||||
*/
|
*/
|
||||||
ctx.addEventListener('fetch', (event: FetchEvent): void => {
|
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) {
|
switch(scope) {
|
||||||
|
case 'download':
|
||||||
case 'thumb':
|
case 'thumb':
|
||||||
case 'document':
|
case 'document':
|
||||||
case 'photo': {
|
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.request.signal.onabort = (e) => {
|
||||||
event.respondWith(promise);
|
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;
|
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': {
|
case 'upload': {
|
||||||
if(event.request.method == 'POST') {
|
if(event.request.method == 'POST') {
|
||||||
event.respondWith(event.request.blob().then(blob => {
|
event.respondWith(event.request.blob().then(blob => {
|
||||||
@ -273,3 +422,24 @@ ctx.addEventListener('fetch', (event: FetchEvent): void => {
|
|||||||
else event.respondWith(fetch(event.request.url)); */
|
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 AppStorage from '../storage';
|
||||||
import CryptoWorkerMethods from '../crypto/crypto_methods';
|
import CryptoWorkerMethods from '../crypto/crypto_methods';
|
||||||
import runtime from 'serviceworker-webpack-plugin/lib/runtime';
|
import runtime from 'serviceworker-webpack-plugin/lib/runtime';
|
||||||
import { InputFileLocation, FileLocation } from '../../types';
|
import { InputFileLocation, FileLocation } from '../../types';
|
||||||
|
import { logger } from '../logger';
|
||||||
|
|
||||||
type Task = {
|
type Task = {
|
||||||
taskID: number,
|
taskID: number,
|
||||||
@ -10,14 +11,6 @@ type Task = {
|
|||||||
args: any[]
|
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 {
|
class ApiManagerProxy extends CryptoWorkerMethods {
|
||||||
private taskID = 0;
|
private taskID = 0;
|
||||||
private awaiting: {
|
private awaiting: {
|
||||||
@ -28,30 +21,37 @@ class ApiManagerProxy extends CryptoWorkerMethods {
|
|||||||
}
|
}
|
||||||
} = {} as any;
|
} = {} as any;
|
||||||
private pending: Array<Task> = [];
|
private pending: Array<Task> = [];
|
||||||
private debug = false;
|
|
||||||
|
|
||||||
public updatesProcessor: (obj: any, bool: boolean) => void = null;
|
public updatesProcessor: (obj: any, bool: boolean) => void = null;
|
||||||
|
|
||||||
|
private log = logger('API-PROXY');
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
console.log(dT(), 'ApiManagerProxy constructor');
|
this.log('constructor');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service worker
|
* 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) => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
console.info(dT(), 'ApiManagerProxy set SW');
|
this.log('set SW');
|
||||||
this.releasePending();
|
this.releasePending();
|
||||||
|
|
||||||
|
//registration.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||||
console.warn(dT(), 'ApiManagerProxy controllerchange');
|
this.log.warn('controllerchange');
|
||||||
this.releasePending();
|
this.releasePending();
|
||||||
|
|
||||||
navigator.serviceWorker.controller.addEventListener('error', (e) => {
|
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) {
|
if(this.updatesProcessor) {
|
||||||
this.updatesProcessor(e.data.update.obj, e.data.update.bool);
|
this.updatesProcessor(e.data.update.obj, e.data.update.bool);
|
||||||
}
|
}
|
||||||
|
} else if(e.data.progress) {
|
||||||
|
$rootScope.$broadcast('download_progress', e.data.progress);
|
||||||
} else {
|
} else {
|
||||||
this.finalizeTask(e.data.taskID, e.data.result, e.data.error);
|
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) {
|
private finalizeTask(taskID: number, result: any, error: any) {
|
||||||
let deferred = this.awaiting[taskID];
|
let deferred = this.awaiting[taskID];
|
||||||
if(deferred !== undefined) {
|
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);
|
result === undefined ? deferred.reject(error) : deferred.resolve(result);
|
||||||
delete this.awaiting[taskID];
|
delete this.awaiting[taskID];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public performTaskWorker<T>(task: string, ...args: any[]) {
|
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) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
this.awaiting[this.taskID] = {resolve, reject, taskName: task};
|
this.awaiting[this.taskID] = {resolve, reject, taskName: task};
|
||||||
@ -168,6 +170,10 @@ class ApiManagerProxy extends CryptoWorkerMethods {
|
|||||||
}> = {}): Promise<Blob> {
|
}> = {}): Promise<Blob> {
|
||||||
return this.performTaskWorker('downloadFile', dcID, location, size, options);
|
return this.performTaskWorker('downloadFile', dcID, location, size, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public cancelDownload(fileName: string) {
|
||||||
|
return this.performTaskWorker('cancelDownload', fileName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiManagerProxy = new ApiManagerProxy();
|
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
|
// @ts-ignore
|
||||||
import {SecureRandom} from 'jsbn';
|
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) {
|
String.prototype.toHHMMSS = function(leadZero = false) {
|
||||||
let sec_num = parseInt(this + '', 10);
|
const sec_num = parseInt(this + '', 10);
|
||||||
let hours: any = Math.floor(sec_num / 3600);
|
const hours = Math.floor(sec_num / 3600);
|
||||||
let minutes: any = Math.floor((sec_num - (hours * 3600)) / 60);
|
let minutes: any = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||||
let seconds: any = sec_num - (hours * 3600) - (minutes * 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(minutes < 10) minutes = leadZero ? "0" + minutes : minutes;
|
||||||
if(seconds < 10) seconds = "0" + seconds;
|
if(seconds < 10) seconds = "0" + seconds;
|
||||||
return minutes + ':' + seconds;
|
return (hours ? /* ('0' + hours).slice(-2) */hours + ':' : '') + minutes + ':' + seconds;
|
||||||
}
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Uint8Array {
|
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 = {
|
export const $rootScope = {
|
||||||
$broadcast: (name: string, detail?: any) => {
|
$broadcast: (name: BroadcastKeys, detail?: any) => {
|
||||||
if(name != 'user_update') {
|
if(name != 'user_update') {
|
||||||
console.debug(dT(), 'Broadcasting ' + name + ' event, with args:', detail);
|
console.debug(dT(), 'Broadcasting ' + name + ' event, with args:', detail);
|
||||||
}
|
}
|
||||||
@ -160,9 +170,14 @@ export const $rootScope = {
|
|||||||
let myCustomEvent = new CustomEvent(name, {detail});
|
let myCustomEvent = new CustomEvent(name, {detail});
|
||||||
document.dispatchEvent(myCustomEvent);
|
document.dispatchEvent(myCustomEvent);
|
||||||
},
|
},
|
||||||
$on: (name: string, callback: any) => {
|
$on: (name: BroadcastKeys, callback: (e: CustomEvent) => any) => {
|
||||||
|
// @ts-ignore
|
||||||
document.addEventListener(name, callback);
|
document.addEventListener(name, callback);
|
||||||
},
|
},
|
||||||
|
$off: (name: BroadcastKeys, callback: (e: CustomEvent) => any) => {
|
||||||
|
// @ts-ignore
|
||||||
|
document.removeEventListener(name, callback);
|
||||||
|
},
|
||||||
|
|
||||||
selectedPeerID: 0,
|
selectedPeerID: 0,
|
||||||
myID: 0,
|
myID: 0,
|
||||||
@ -523,11 +538,12 @@ export function getEmojiToneIndex(input: string) {
|
|||||||
return match ? 5 - (57343 - match[0].charCodeAt(0)) : 0;
|
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,
|
dcID: number,
|
||||||
location: InputFileLocation | FileLocation,
|
location: InputFileLocation | FileLocation,
|
||||||
size?: number,
|
size?: number,
|
||||||
mimeType?: string
|
mimeType?: string,
|
||||||
|
fileName?: string
|
||||||
}) {
|
}) {
|
||||||
//console.log('getFileURL', location);
|
//console.log('getFileURL', location);
|
||||||
//const perf = performance.now();
|
//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 {
|
&__loaded {
|
||||||
background: rgba(255, 255, 255, 0.38);
|
background: rgba(255, 255, 255, 0.38);
|
||||||
position: absolute;
|
left: 11px;
|
||||||
width: calc(100% - 16px);
|
width: calc(100% - 11px);
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -273,6 +271,13 @@ video::-webkit-media-controls-enclosure {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__loaded {
|
||||||
|
position: absolute;
|
||||||
|
left: 12px;
|
||||||
|
top: 0;
|
||||||
|
width: calc(100% - 12px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type=range] {
|
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,
|
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 = {
|
export type FileLocation = {
|
||||||
_: 'fileLocationToBeDeprecated',
|
_: 'fileLocationToBeDeprecated',
|
||||||
volume_id: string,
|
volume_id: string,
|
||||||
|
@ -73,8 +73,7 @@ module.exports = merge(common, {
|
|||||||
|| file.includes('pako')
|
|| file.includes('pako')
|
||||||
|| file.includes('Worker.min.js')
|
|| file.includes('Worker.min.js')
|
||||||
|| file.includes('recorder.min.js')
|
|| file.includes('recorder.min.js')
|
||||||
|| file.includes('.hbs')
|
|| file.includes('.hbs')) return;
|
||||||
|| file.includes('mp4box')) return;
|
|
||||||
|
|
||||||
let p = path.resolve(buildDir + file);
|
let p = path.resolve(buildDir + file);
|
||||||
if(!newlyCreatedAssets[file] && ['.gz', '.js'].find(ext => file.endsWith(ext)) !== undefined) {
|
if(!newlyCreatedAssets[file] && ['.gz', '.js'].find(ext => file.endsWith(ext)) !== undefined) {
|
||||||
|
Loading…
Reference in New Issue
Block a user