Browse Source

SW:

Downloads;
Audio & video streaming;
Download controller
master
morethanwords 4 years ago
parent
commit
617acdbb13
  1. 9
      package-lock.json
  2. 1
      package.json
  3. 6
      src/components/animationIntersector.ts
  4. 18
      src/components/appAudio.ts
  5. 103
      src/components/audio.ts
  6. 123
      src/components/preloader_new.ts
  7. 41
      src/components/wrappers.ts
  8. 326
      src/lib/MP4Source.ts
  9. 4
      src/lib/appManagers/appDialogsManager.ts
  10. 114
      src/lib/appManagers/appDocsManager.ts
  11. 115
      src/lib/appManagers/appDownloadManager.ts
  12. 9
      src/lib/appManagers/appImManager.ts
  13. 63
      src/lib/appManagers/appMediaViewer.ts
  14. 86
      src/lib/appManagers/appPhotosManager.ts
  15. 128
      src/lib/filemanager.ts
  16. 34
      src/lib/libwebp.js
  17. 461
      src/lib/lottieLoader copy.ts
  18. 33
      src/lib/mediaPlayer.ts
  19. 183
      src/lib/mtproto/apiFileManager.ts
  20. 202
      src/lib/mtproto/mtproto.service.ts
  21. 40
      src/lib/mtproto/mtprotoworker.ts
  22. 12
      src/lib/polyfill.ts
  23. 24
      src/lib/utils.ts
  24. 24
      src/lib/webp_bad.js
  25. 13
      src/scss/partials/_ckin.scss
  26. 11
      src/types.d.ts
  27. 3
      webpack.prod.js

9
package-lock.json generated

@ -9688,15 +9688,6 @@ @@ -9688,15 +9688,6 @@
}
}
},
"mp4box": {
"version": "0.3.20",
"resolved": "https://registry.npmjs.org/mp4box/-/mp4box-0.3.20.tgz",
"integrity": "sha512-9I1wOBql0c9BsIPDGHY97dcH5kT7hG0Tx6SAaJvXf+A6Z0zBfGy7L1vEfjMKgjXSjtdXWL7gO+8a5euikaFTEA==",
"dev": true,
"requires": {
"npm": "^6.9.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",

1
package.json

@ -50,7 +50,6 @@ @@ -50,7 +50,6 @@
"lottie-web": "^5.6.10",
"media-query-plugin": "^1.3.1",
"mini-css-extract-plugin": "^0.9.0",
"mp4box": "^0.3.20",
"node-sass": "^4.14.1",
"npm": "^6.14.5",
"on-build-webpack": "^0.1.0",

6
src/components/animationIntersector.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { isInDOM } from "../lib/utils";
import { isInDOM, $rootScope } from "../lib/utils";
import { RLottiePlayer } from "../lib/lottieLoader";
export interface AnimationItem {
@ -18,6 +18,8 @@ export class AnimationIntersector { @@ -18,6 +18,8 @@ export class AnimationIntersector {
constructor() {
this.observer = new IntersectionObserver((entries) => {
if($rootScope.idle.isIDLE) return;
for(const entry of entries) {
const target = entry.target;
@ -72,6 +74,8 @@ export class AnimationIntersector { @@ -72,6 +74,8 @@ export class AnimationIntersector {
}
public checkAnimations(blurred?: boolean, group?: string, destroy = false) {
if($rootScope.idle.isIDLE) return;
const groups = group /* && false */ ? [group] : Object.keys(this.byGroups);
if(group && !this.byGroups[group]) {

18
src/components/appAudio.ts

@ -48,11 +48,8 @@ class AppAudio { @@ -48,11 +48,8 @@ class AppAudio {
audio.addEventListener('pause', this.onPause);
audio.addEventListener('ended', this.onEnded);
appDocsManager.downloadDoc(doc.id).then(() => {
this.container.append(audio);
source.src = doc.url;
}, () => {
const onError = (e: Event) => {
if(this.nextMid == mid) {
this.loadSiblingsAudio(doc.type as 'voice' | 'audio', mid).then(() => {
if(this.nextMid && this.audios[this.nextMid]) {
@ -60,7 +57,16 @@ class AppAudio { @@ -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;
}

103
src/components/audio.ts

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
import appDocsManager from "../lib/appManagers/appDocsManager";
import { RichTextProcessor } from "../lib/richtextprocessor";
import { formatDate } from "./wrappers";
import ProgressivePreloader from "./preloader";
import { CancellablePromise } from "../lib/polyfill";
import ProgressivePreloader from "./preloader_new";
import { MediaProgressLine } from "../lib/mediaPlayer";
import appAudio from "./appAudio";
import { MTDocument } from "../types";
import { mediaSizes } from "../lib/config";
import { Download } from "../lib/appManagers/appDownloadManager";
// https://github.com/LonamiWebs/Telethon/blob/4393ec0b83d511b6a20d8a20334138730f084375/telethon/utils.py#L1285
export function decodeWaveform(waveform: Uint8Array | number[]) {
@ -233,7 +233,7 @@ function wrapAudio(doc: MTDocument, audioEl: AudioElement) { @@ -233,7 +233,7 @@ function wrapAudio(doc: MTDocument, audioEl: AudioElement) {
const subtitleDiv = audioEl.querySelector('.audio-subtitle') as HTMLDivElement;
let launched = false;
let progressLine = new MediaProgressLine(audioEl.audio);
let progressLine = new MediaProgressLine(audioEl.audio, doc.supportsStreaming);
audioEl.addAudioListener('ended', () => {
audioEl.classList.remove('audio-show-progress');
@ -295,19 +295,23 @@ export default class AudioElement extends HTMLElement { @@ -295,19 +295,23 @@ export default class AudioElement extends HTMLElement {
const durationStr = String(doc.duration | 0).toHHMMSS(true);
this.innerHTML = `
<div class="audio-toggle audio-ico tgico-largeplay"></div>
<div class="audio-download">${uploading ? '' : '<div class="tgico-download"></div>'}</div>`;
this.innerHTML = `<div class="audio-toggle audio-ico tgico-largeplay"></div>`;
const onTypeLoad = doc.type == 'voice' ? wrapVoiceMessage(doc, this) : wrapAudio(doc, this);
const downloadDiv = document.createElement('div');
downloadDiv.classList.add('audio-download');
if(!uploading && doc.type != 'audio') {
downloadDiv.innerHTML = '<div class="tgico-download"></div>';
}
const downloadDiv = this.querySelector('.audio-download') as HTMLDivElement;
if(doc.type != 'audio' && !uploading) {
this.append(downloadDiv);
}
const onTypeLoad = doc.type == 'voice' ? wrapVoiceMessage(doc, this) : wrapAudio(doc, this);
const audioTimeDiv = this.querySelector('.audio-time') as HTMLDivElement;
audioTimeDiv.innerHTML = durationStr;
let preloader: ProgressivePreloader = this.preloader;
let promise: CancellablePromise<Blob>;
const onLoad = () => {
const audio = this.audio = appAudio.addAudio(doc, mid);
@ -351,35 +355,64 @@ export default class AudioElement extends HTMLElement { @@ -351,35 +355,64 @@ export default class AudioElement extends HTMLElement {
};
if(!uploading) {
const onClick = () => {
if(!promise) {
let preloader: ProgressivePreloader = this.preloader;
if(doc.type == 'voice') {
let download: Download;
const onClick = () => {
if(!download) {
if(!preloader) {
preloader = new ProgressivePreloader(null, true);
}
download = appDocsManager.downloadDocNew(doc.id);
preloader.attach(downloadDiv, true, appDocsManager.getInputFileName(doc));
download.promise.then(() => {
downloadDiv.remove();
this.removeEventListener('click', onClick);
onLoad();
}).catch(err => {
if(err.name === 'AbortError') {
download = null;
}
}).finally(() => {
downloadDiv.classList.remove('downloading');
});
downloadDiv.classList.add('downloading');
} else {
download.controller.abort();
}
};
this.addEventListener('click', onClick);
this.click();
} else {
const r = () => {
onLoad();
if(!preloader) {
preloader = new ProgressivePreloader(null, true);
preloader = new ProgressivePreloader(null, false);
}
promise = appDocsManager.downloadDoc(doc.id);
preloader.attach(downloadDiv, true, promise);
promise.then(() => {
preloader = null;
downloadDiv.classList.remove('downloading');
preloader.attach(downloadDiv);
this.append(downloadDiv);
new Promise((resolve) => {
if(this.audio.readyState >= 2) resolve();
else this.addAudioListener('canplay', resolve);
}).then(() => {
downloadDiv.remove();
this.removeEventListener('click', onClick);
onLoad();
this.audio.play();
});
downloadDiv.classList.add('downloading');
} else {
downloadDiv.classList.remove('downloading');
promise.cancel();
promise = null;
}
};
this.addEventListener('click', onClick);
this.click();
};
this.addEventListener('click', r, {once: true});
}
} else {
this.preloader.attach(this.querySelector('.audio-download'), false);
this.preloader.attach(downloadDiv, false);
//onLoad();
}
}

123
src/components/preloader_new.ts

@ -0,0 +1,123 @@ @@ -0,0 +1,123 @@
import { isInDOM, $rootScope, cancelEvent } from "../lib/utils";
import appDownloadManager, { Progress } from "../lib/appManagers/appDownloadManager";
export default class ProgressivePreloader {
public preloader: HTMLDivElement;
private circle: SVGCircleElement;
//private tempID = 0;
private detached = true;
private fileName: string;
public controller: AbortController;
constructor(elem?: Element, private cancelable = true) {
this.preloader = document.createElement('div');
this.preloader.classList.add('preloader-container');
this.preloader.innerHTML = `
<div class="you-spin-me-round">
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-circular" viewBox="25 25 50 50">
<circle class="preloader-path-new" cx="50" cy="50" r="23" fill="none" stroke-miterlimit="10"/>
</svg>
</div>`;
if(cancelable) {
this.preloader.innerHTML += `
<svg xmlns="http://www.w3.org/2000/svg" class="preloader-close" viewBox="0 0 20 20">
<line x1="0" y1="20" x2="20" y2="0" stroke-width="2" stroke-linecap="round"></line>
<line x1="0" y1="0" x2="20" y2="20" stroke-width="2" stroke-linecap="round"></line>
</svg>`;
} else {
this.preloader.classList.add('preloader-swing');
}
this.circle = this.preloader.firstElementChild.firstElementChild.firstElementChild as SVGCircleElement;
if(elem) {
this.attach(elem);
}
if(this.cancelable) {
this.preloader.addEventListener('click', (e) => {
cancelEvent(e);
this.detach();
if(!this.fileName) {
return;
}
const download = appDownloadManager.getDownload(this.fileName);
if(download && download.controller && !download.controller.signal.aborted) {
download.controller.abort();
}
});
}
}
downloadProgressHandler = (details: Progress) => {
if(details.done >= details.total) {
this.detach();
}
//console.log('preloader download', promise, details);
let percents = details.done / details.total * 100;
this.setProgress(percents);
};
public attach(elem: Element, reset = true, fileName?: string, append = true) {
this.fileName = fileName;
if(this.fileName) {
const download = appDownloadManager.getDownload(fileName);
download.promise.catch(() => {
this.detach();
});
appDownloadManager.addProgressCallback(this.fileName, this.downloadProgressHandler);
}
this.detached = false;
window.requestAnimationFrame(() => {
if(this.detached) return;
this.detached = false;
elem[append ? 'append' : 'prepend'](this.preloader);
if(this.cancelable && reset) {
this.setProgress(0);
}
});
}
public detach() {
this.detached = true;
if(this.preloader.parentElement) {
window.requestAnimationFrame(() => {
if(!this.detached) return;
this.detached = true;
if(this.preloader.parentElement) {
this.preloader.parentElement.removeChild(this.preloader);
}
});
}
}
public setProgress(percents: number) {
if(!isInDOM(this.circle)) {
return;
}
if(percents == 0) {
this.circle.style.strokeDasharray = '';
return;
}
try {
let totalLength = this.circle.getTotalLength();
//console.log('setProgress', (percents / 100 * totalLength));
this.circle.style.strokeDasharray = '' + Math.max(5, percents / 100 * totalLength) + ', 200';
} catch(err) {}
}
}

41
src/components/wrappers.ts

@ -5,6 +5,7 @@ import LottieLoader from '../lib/lottieLoader'; @@ -5,6 +5,7 @@ import LottieLoader from '../lib/lottieLoader';
import appDocsManager from "../lib/appManagers/appDocsManager";
import { formatBytes, getEmojiToneIndex } from "../lib/utils";
import ProgressivePreloader from './preloader';
import ProgressivePreloaderNew from './preloader_new';
import LazyLoadQueue from './lazyLoadQueue';
import VideoPlayer from '../lib/mediaPlayer';
import { RichTextProcessor } from '../lib/richtextprocessor';
@ -18,6 +19,7 @@ import { mediaSizes } from '../lib/config'; @@ -18,6 +19,7 @@ import { mediaSizes } from '../lib/config';
import { MTDocument, MTPhotoSize } from '../types';
import animationIntersector from './animationIntersector';
import AudioElement from './audio';
import { Download } from '../lib/appManagers/appDownloadManager';
export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue}: {
doc: MTDocument,
@ -93,14 +95,14 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai @@ -93,14 +95,14 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai
if(message.media.preloader) { // means upload
(message.media.preloader as ProgressivePreloader).attach(container, undefined, undefined, false);
} else if(!doc.downloaded) {
const promise = appDocsManager.downloadDoc(doc.id);
/* const promise = appDocsManager.downloadDoc(doc.id);
//if(!doc.supportsStreaming) {
const preloader = new ProgressivePreloader(container, true);
preloader.attach(container, true, promise, false);
//}
await promise;
await promise; */
}
if(middleware && !middleware()) {
@ -215,34 +217,35 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals @@ -215,34 +217,35 @@ export function wrapDocument(doc: MTDocument, withTime = false, uploading = fals
if(!uploading) {
let downloadDiv = docDiv.querySelector('.document-download') as HTMLDivElement;
let preloader: ProgressivePreloader;
let promise: CancellablePromise<Blob>;
let preloader: ProgressivePreloaderNew;
let download: Download;
docDiv.addEventListener('click', () => {
if(!promise) {
if(!download) {
if(downloadDiv.classList.contains('downloading')) {
return; // means not ready yet
}
if(!preloader) {
preloader = new ProgressivePreloader(null, true);
preloader = new ProgressivePreloaderNew(null, true);
}
download = appDocsManager.saveDocFile(doc);
preloader.attach(downloadDiv, true, appDocsManager.getInputFileName(doc));
appDocsManager.saveDocFile(doc.id).then(res => {
promise = res.promise;
preloader.attach(downloadDiv, true, promise);
promise.then(() => {
downloadDiv.classList.remove('downloading');
downloadDiv.remove();
});
})
download.promise.then(() => {
downloadDiv.remove();
}).catch(err => {
if(err.name === 'AbortError') {
download = null;
}
}).finally(() => {
downloadDiv.classList.remove('downloading');
});
downloadDiv.classList.add('downloading');
} else {
downloadDiv.classList.remove('downloading');
promise = null;
download.controller.abort();
}
});
}
@ -480,7 +483,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o @@ -480,7 +483,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o
}
let downloaded = doc.downloaded;
let load = () => appDocsManager.downloadDoc(doc.id).then(blob => {
let load = () => appDocsManager.downloadDocNew(doc.id).promise.then(blob => {
//console.log('loaded sticker:', doc, div);
if(middleware && !middleware()) return;

326
src/lib/MP4Source.ts

@ -1,326 +0,0 @@ @@ -1,326 +0,0 @@
/*
* Copyright (c) 2018-present, Evgeny Nadymov
*
* This source code is licensed under the GPL v.3.0 license found in the
* LICENSE file in the root directory of this source tree.
*/
// @ts-ignore
//import MP4Box from 'mp4box/dist/mp4box.all.min';
import { logger, LogLevels } from './logger';
export default class MP4Source {
private mp4file: any;
private nextBufferStart = 0;
private mediaSource: MediaSource = null;
private ready = false;
private bufferedTime = 40;
private beforeMoovBufferSize = 32 * 1024;
private moovBufferSize = 512 * 1024;
private bufferSize = 512 * 1024;
private seekBufferSize = 256 * 1024;
private currentBufferSize = this.beforeMoovBufferSize;
private nbSamples = 10;
private expectedSize: number;
private seeking = false;
private loading = false;
private url: string;
private log = logger('MP4'/* , LogLevels.error */);
//public onLoadBuffer: (offset: number)
constructor(private video: {duration: number, video: {expected_size: number}}, private getBufferAsync: (start: number, end: number) => Promise<ArrayBuffer>) {
this.expectedSize = this.video.video.expected_size;
this.init(video.duration);
}
init(videoDuration: number) {
const mediaSource = new MediaSource();
mediaSource.addEventListener('sourceopen', () => {
this.log('[MediaSource] sourceopen start', this.mediaSource, this);
if(this.mediaSource.sourceBuffers.length > 0) return;
//const mp4File = MP4Box.createFile();
const mp4File = (window as any).MP4Box.createFile();
mp4File.onMoovStart = () => {
this.log('[MP4Box] onMoovStart');
this.currentBufferSize = this.moovBufferSize;
};
mp4File.onError = (error: Error) => {
this.log('[MP4Box] onError', error);
};
mp4File.onReady = (info: any) => {
this.log('[MP4Box] onReady', info);
this.ready = true;
this.currentBufferSize = this.bufferSize;
const { isFragmented, timescale, fragment_duration, duration } = info;
if(!fragment_duration && !duration) {
this.mediaSource.duration = videoDuration;
this.bufferedTime = videoDuration;
} else {
this.mediaSource.duration = isFragmented
? fragment_duration / timescale
: duration / timescale;
}
this.initializeAllSourceBuffers(info);
};
mp4File.onSegment = (id: number, sb: any, buffer: ArrayBuffer, sampleNum: number, is_last: boolean) => {
const isLast = (sampleNum + this.nbSamples) > sb.nb_samples;
this.log('[MP4Box] onSegment', id, buffer, `${sampleNum}/${sb.nb_samples}`, isLast, sb.timestampOffset, mediaSource, is_last);
sb.segmentIndex++;
sb.pendingAppends.push({ id, buffer, sampleNum, is_last: isLast });
this.onUpdateEnd(sb, true, false);
};
this.mp4file = mp4File;
this.log('[MediaSource] sourceopen end', this, this.mp4file);
this.loadNextBuffer();
});
mediaSource.addEventListener('sourceended', () => {
this.log('[MediaSource] sourceended', mediaSource.readyState);
//this.getBufferAsync = null;
});
mediaSource.addEventListener('sourceclose', () => {
this.log('[MediaSource] sourceclose', mediaSource.readyState);
//this.getBufferAsync = null;
});
this.mediaSource = mediaSource;
}
private onInitAppended(sb: any) {
sb.sampleNum = 0;
sb.addEventListener('updateend', () => this.onUpdateEnd(sb, true, true));
/* In case there are already pending buffers we call onUpdateEnd to start appending them*/
this.onUpdateEnd(sb, false, true);
// @ts-ignore
this.mediaSource.pendingInits--;
// @ts-ignore
if(this.mediaSource.pendingInits === 0) {
this.log('onInitAppended start!');
this.mp4file.start();
if(this.expectedSize > this.bufferSize) {
this.nextBufferStart = this.bufferSize;
} else {
return;
}
/* setInterval(() => {
this.loadNextBuffer();
}, 1e3); */
this.loadNextBuffer();
}
};
private onUpdateEnd(sb: any, isNotInit: boolean, isEndOfAppend: boolean) {
//console.this.log('onUpdateEnd', sb, isNotInit, isEndOfAppend, sb.sampleNum, sb.is_last);
if(isEndOfAppend === true) {
if(sb.sampleNum) {
this.mp4file.releaseUsedSamples(sb.id, sb.sampleNum);
delete sb.sampleNum;
}
if(sb.is_last) {
this.log('onUpdateEnd', sb, isNotInit, isEndOfAppend, sb.sampleNum, sb.is_last);
this.mediaSource.endOfStream();
}
}
if(this.mediaSource.readyState === "open" && sb.updating === false && sb.pendingAppends.length > 0) {
const obj = sb.pendingAppends.shift();
this.log("MSE - SourceBuffer #"+sb.id, "Appending new buffer, pending: "+sb.pendingAppends.length);
sb.sampleNum = obj.sampleNum;
sb.is_last = obj.is_last;
sb.appendBuffer(obj.buffer);
}
}
private initializeAllSourceBuffers(info: any) {
for(let i = 0; i < info.tracks.length; i++) {
this.addSourceBuffer(info.tracks[i]);
}
this.initializeSourceBuffers();
}
private initializeSourceBuffers() {
const initSegs = this.mp4file.initializeSegmentation();
this.log('[MP4Box] initializeSegmentation', initSegs);
for(let i = 0; i < initSegs.length; i++) {
const sb: any = initSegs[i].user;
if(i === 0) {
// @ts-ignore
this.mediaSource.pendingInits = 0;
}
let onInitAppended = () => {
if(this.mediaSource.readyState === "open") {
sb.removeEventListener('updateend', onInitAppended);
this.onInitAppended(sb);
}
};
sb.addEventListener('updateend', onInitAppended);
sb.appendBuffer(initSegs[i].buffer);
sb.segmentIndex = 0;
// @ts-ignore
this.mediaSource.pendingInits++;
}
}
private addSourceBuffer(track: {id: number, codec: string, type: 'video', nb_samples: number}) {
const file = this.mp4file;
const ms = this.mediaSource;
if(!track) return;
const { id, codec, type: trackType, nb_samples } = track;
const mime = `video/mp4; codecs="${codec}"`;
this.log('mimetype:', mime);
if(!MediaSource.isTypeSupported(mime)) {
this.log('[addSourceBuffer] not supported', mime);
return;
}
const sb: any = ms.addSourceBuffer(mime);
sb.id = id;
sb.pendingAppends = [];
sb.nb_samples = nb_samples;
file.setSegmentOptions(id, sb, { nbSamples: this.nbSamples });
this.log('[addSourceBuffer] add', id, codec, trackType, sb);
sb.addEventListener("error", (e: Event) => {
this.log("MSE SourceBuffer #" + id, e);
});
}
stop() {
this.mp4file.stop();
this.mp4file = null;
this.getBufferAsync = null;
}
getURL() {
return this.url ?? (this.url = URL.createObjectURL(this.mediaSource));
}
seek(currentTime: number/* , buffered: any */) {
const seekInfo: {offset: number, time: number} = this.mp4file.seek(currentTime, true);
this.nextBufferStart = seekInfo.offset;
const loadNextBuffer = true;
/* let loadNextBuffer = buffered.length === 0;
for(let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if(start <= currentTime && currentTime + this.bufferedTime > end) {
loadNextBuffer = true;
break;
}
} */
this.log('[player] onSeeked', loadNextBuffer, currentTime, seekInfo, this.nextBufferStart);
if(loadNextBuffer) {
this.loadNextBuffer(true);
}
return seekInfo.offset;
}
timeUpdate(currentTime: number, duration: number, buffered: any) {
//return;
const ranges = [];
for(let i = 0; i < buffered.length; i++) {
ranges.push({ start: buffered.start(i), end: buffered.end(i)})
}
let loadNextBuffer = buffered.length === 0;
let hasRange = false;
for(let i = 0; i < buffered.length; i++) {
const start = buffered.start(i);
const end = buffered.end(i);
if (start <= currentTime && currentTime <= end) {
hasRange = true;
if (end < duration && currentTime + this.bufferedTime > end) {
loadNextBuffer = true;
break;
}
}
}
if(!hasRange) {
loadNextBuffer = true;
}
this.log('[player] timeUpdate', loadNextBuffer, currentTime, duration, JSON.stringify(ranges));
if(loadNextBuffer) {
this.loadNextBuffer();
}
}
async loadNextBuffer(seek = false) {
const { nextBufferStart, loading, currentBufferSize, mp4file } = this;
this.log('[player] loadNextBuffer', nextBufferStart === undefined, loading, !mp4file);
if(!mp4file) return;
if(nextBufferStart === undefined) return;
if(loading) return;
//return;
this.loading = true;
let bufferSize = seek ? this.seekBufferSize : this.bufferSize;
if(nextBufferStart + bufferSize > this.expectedSize) {
bufferSize = this.expectedSize - nextBufferStart;
}
const nextBuffer = await this.getBufferAsync(nextBufferStart, nextBufferStart + bufferSize);
// @ts-ignore
nextBuffer.fileStart = nextBufferStart;
const end = (nextBuffer.byteLength !== bufferSize)/* || (nextBuffer.byteLength === this.expectedSize) */;
this.log('[player] loadNextBuffer start', nextBuffer.byteLength, nextBufferStart, end);
if(nextBuffer.byteLength) {
this.nextBufferStart = mp4file.appendBuffer(nextBuffer/* , end */);
} else {
this.nextBufferStart = undefined;
}
if(end) {
this.log('[player] loadNextBuffer flush');
this.mp4file.flush();
}
this.log('[player] loadNextBuffer stop', nextBuffer.byteLength, nextBufferStart, this.nextBufferStart);
this.loading = false;
if(!this.ready || !end) {
this.log('[player] loadNextBuffer next');
this.loadNextBuffer();
}
}
}
(window as any).MP4Source = MP4Source;

4
src/lib/appManagers/appDialogsManager.ts

@ -924,7 +924,7 @@ export class AppDialogsManager { @@ -924,7 +924,7 @@ export class AppDialogsManager {
dom = this.getDialogDom(dialog.peerID);
if(!dom) {
this.log.error('no dom for dialog:', dialog, lastMessage, dom, highlightWord);
//this.log.error('no dom for dialog:', dialog, lastMessage, dom, highlightWord);
return;
}
}
@ -1034,7 +1034,7 @@ export class AppDialogsManager { @@ -1034,7 +1034,7 @@ export class AppDialogsManager {
}
if(!dom) {
this.log.error('setUnreadMessages no dom!', dialog);
//this.log.error('setUnreadMessages no dom!', dialog);
return;
}

114
src/lib/appManagers/appDocsManager.ts

@ -1,15 +1,16 @@ @@ -1,15 +1,16 @@
import FileManager from '../filemanager';
import {RichTextProcessor} from '../richtextprocessor';
import { CancellablePromise, deferredPromise } from '../polyfill';
import { isObject, getFileURL } from '../utils';
import opusDecodeController from '../opusDecodeController';
import { MTDocument, inputDocumentFileLocation } from '../../types';
import { getFileNameByLocation } from '../bin_utils';
import appDownloadManager, { Download } from './appDownloadManager';
class AppDocsManager {
private docs: {[docID: string]: MTDocument} = {};
private thumbs: {[docIDAndSize: string]: Promise<string>} = {};
private downloadPromises: {[docID: string]: CancellablePromise<Blob>} = {};
public saveDoc(apiDoc: MTDocument, context?: any) {
//console.log('saveDoc', apiDoc, this.docs[apiDoc.id]);
if(this.docs[apiDoc.id]) {
@ -47,13 +48,17 @@ class AppDocsManager { @@ -47,13 +48,17 @@ class AppDocsManager {
apiDoc.audioTitle = attribute.title;
apiDoc.audioPerformer = attribute.performer;
apiDoc.type = attribute.pFlags.voice && apiDoc.mime_type == "audio/ogg" ? 'voice' : 'audio';
if(apiDoc.type == 'audio') {
apiDoc.supportsStreaming = true;
}
break;
case 'documentAttributeVideo':
apiDoc.duration = attribute.duration;
apiDoc.w = attribute.w;
apiDoc.h = attribute.h;
apiDoc.supportsStreaming = attribute.pFlags?.supports_streaming && apiDoc.size > 524288 && typeof(MediaSource) !== 'undefined';
apiDoc.supportsStreaming = attribute.pFlags?.supports_streaming/* && apiDoc.size > 524288 */;
if(apiDoc.thumbs && attribute.pFlags.round_message) {
apiDoc.type = 'round';
} else /* if(apiDoc.thumbs) */ {
@ -134,6 +139,10 @@ class AppDocsManager { @@ -134,6 +139,10 @@ class AppDocsManager {
apiDoc.size = 0;
}
if(!apiDoc.url) {
apiDoc.url = this.getFileURLByDoc(apiDoc);
}
return apiDoc;
}
@ -181,9 +190,21 @@ class AppDocsManager { @@ -181,9 +190,21 @@ class AppDocsManager {
return 't_' + (doc.type || 'file') + doc.id + fileExt;
}
public getFileURLByDoc(doc: MTDocument) {
public getFileURLByDoc(doc: MTDocument, download = false) {
const inputFileLocation = this.getInputByID(doc);
return getFileURL('document', {dcID: doc.dc_id, location: inputFileLocation, size: doc.size, mimeType: doc.mime_type || 'application/octet-stream'});
const type = download ? 'download' : (doc.supportsStreaming ? 'stream' : 'document');
return getFileURL(type, {
dcID: doc.dc_id,
location: inputFileLocation,
size: doc.size,
mimeType: doc.mime_type || 'application/octet-stream',
fileName: doc.file_name
});
}
public getInputFileName(doc: MTDocument) {
return getFileNameByLocation(this.getInputByID(doc));
}
public downloadDoc(docID: string | MTDocument, toFileEntry?: any): CancellablePromise<Blob> {
@ -252,6 +273,56 @@ class AppDocsManager { @@ -252,6 +273,56 @@ class AppDocsManager {
return this.downloadPromises[doc.id] = deferred;
}
public downloadDocNew(docID: string | MTDocument, toFileEntry?: any): Download {
const doc = this.getDoc(docID);
if(doc._ == 'documentEmpty') {
throw new Error('Document empty!');
}
const fileName = this.getInputFileName(doc);
let download = appDownloadManager.getDownload(fileName);
if(download) {
return download;
}
download = appDownloadManager.download(fileName, doc.url);
//const _download: Download = {...download};
//_download.promise = _download.promise.then(async(blob) => {
download.promise = download.promise.then(async(blob) => {
if(blob) {
doc.downloaded = true;
if(doc.type == 'voice' && !opusDecodeController.isPlaySupported()) {
let reader = new FileReader();
await new Promise((resolve, reject) => {
reader.onloadend = (e) => {
let uint8 = new Uint8Array(e.target.result as ArrayBuffer);
//console.log('sending uint8 to decoder:', uint8);
opusDecodeController.decode(uint8).then(result => {
doc.url = result.url;
resolve();
}, (err) => {
delete doc.downloaded;
reject(err);
});
};
reader.readAsArrayBuffer(blob);
});
}
}
return blob;
});
//return this.downloadPromisesNew[doc.id] = _download;
return download;
}
public downloadDocThumb(docID: any, thumbSize: string) {
let doc = this.getDoc(docID);
@ -273,33 +344,12 @@ class AppDocsManager { @@ -273,33 +344,12 @@ class AppDocsManager {
public hasDownloadedThumb(docID: string, thumbSize: string) {
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 {
let writer = FileManager.chooseSaveFile(fileName, ext, doc.mime_type, doc.size);
await writer.ready;
let promise = this.downloadDoc(docID, writer);
promise.then(() => {
writer.close();
console.log('saved doc', doc);
});
//console.log('got promise from downloadDoc', promise);
return {promise};
} catch(err) {
let promise = this.downloadDoc(docID);
promise.then((blob) => {
FileManager.download(blob, doc.mime_type, fileName)
});
return {promise};
}
public saveDocFile(doc: MTDocument) {
const url = this.getFileURLByDoc(doc, true);
const fileName = this.getInputFileName(doc);
return appDownloadManager.downloadToDisc(fileName, url);
}
}

115
src/lib/appManagers/appDownloadManager.ts

@ -0,0 +1,115 @@ @@ -0,0 +1,115 @@
import { $rootScope } from "../utils";
import apiManager from "../mtproto/mtprotoworker";
export type Download = {promise: Promise<Blob>, controller: AbortController};
export type Progress = {done: number, fileName: string, total: number, offset: number};
export type ProgressCallback = (details: Progress) => void;
export class AppDownloadManager {
private downloads: {[fileName: string]: Download} = {};
private progress: {[fileName: string]: Progress} = {};
private progressCallbacks: {[fileName: string]: Array<ProgressCallback>} = {};
constructor() {
$rootScope.$on('download_progress', (e) => {
const details = e.detail as {done: number, fileName: string, total: number, offset: number};
this.progress[details.fileName] = details;
const callbacks = this.progressCallbacks[details.fileName];
if(callbacks) {
callbacks.forEach(callback => callback(details));
}
});
}
public download(fileName: string, url: string) {
if(this.downloads.hasOwnProperty(fileName)) return this.downloads[fileName];
const controller = new AbortController();
const promise = fetch(url, {signal: controller.signal})
.then(res => res.blob())
.catch(err => { // Только потому что event.request.signal не работает в SW, либо я кривой?
if(err.name === 'AbortError') {
//console.log('Fetch aborted');
apiManager.cancelDownload(fileName);
delete this.downloads[fileName];
delete this.progress[fileName];
delete this.progressCallbacks[fileName];
} else {
//console.error('Uh oh, an error!', err);
}
throw err;
});
//console.log('Will download file:', fileName, url);
promise.finally(() => {
delete this.progressCallbacks[fileName];
});
return this.downloads[fileName] = {promise, controller};
}
public getDownload(fileName: string) {
return this.downloads[fileName];
}
public addProgressCallback(fileName: string, callback: ProgressCallback) {
const progress = this.progress[fileName];
(this.progressCallbacks[fileName] ?? (this.progressCallbacks[fileName] = [])).push(callback);
if(progress) {
callback(progress);
}
}
private createDownloadAnchor(url: string, onRemove?: () => void) {
const a = document.createElement('a');
a.href = url;
a.style.position = 'absolute';
a.style.top = '1px';
a.style.left = '1px';
document.body.append(a);
try {
var clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
a.dispatchEvent(clickEvent);
} catch (e) {
console.error('Download click error', e);
try {
a.click();
} catch (e) {
window.open(url as string, '_blank');
}
}
setTimeout(() => {
a.remove();
onRemove && onRemove();
}, 100);
}
/* public downloadToDisc(fileName: string, url: string) {
this.createDownloadAnchor(url);
return this.download(fileName, url);
} */
public downloadToDisc(fileName: string, url: string) {
const download = this.download(fileName, url);
download.promise.then(blob => {
const objectURL = URL.createObjectURL(blob);
this.createDownloadAnchor(objectURL, () => {
URL.revokeObjectURL(objectURL);
});
});
return download;
}
}
export default new AppDownloadManager();

9
src/lib/appManagers/appImManager.ts

@ -768,16 +768,17 @@ export class AppImManager { @@ -768,16 +768,17 @@ export class AppImManager {
window.addEventListener('blur', () => {
animationIntersector.checkAnimations(true);
this.offline = true;
this.offline = $rootScope.idle.isIDLE = true;
this.updateStatus();
clearInterval(this.updateStatusInterval);
window.addEventListener('focus', () => {
animationIntersector.checkAnimations(false);
this.offline = false;
this.offline = $rootScope.idle.isIDLE = false;
this.updateStatus();
this.updateStatusInterval = window.setInterval(() => this.updateStatus(), 50e3);
// в обратном порядке
animationIntersector.checkAnimations(false);
}, {once: true});
});

63
src/lib/appManagers/appMediaViewer.ts

@ -12,7 +12,6 @@ import AvatarElement from "../../components/avatar"; @@ -12,7 +12,6 @@ import AvatarElement from "../../components/avatar";
import LazyLoadQueue from "../../components/lazyLoadQueue";
import appForward from "../../components/appForward";
import { isSafari, mediaSizes } from "../config";
import MP4Source from "../MP4Source";
export class AppMediaViewer {
public wholeDiv = document.querySelector('.media-viewer-whole') as HTMLDivElement;
@ -806,7 +805,7 @@ export class AppMediaViewer { @@ -806,7 +805,7 @@ export class AppMediaViewer {
video.append(source);
}
const createPlayer = (streamable = false) => {
const createPlayer = () => {
if(media.type != 'gif') {
video.dataset.ckin = 'default';
video.dataset.overlay = '1';
@ -815,7 +814,7 @@ export class AppMediaViewer { @@ -815,7 +814,7 @@ export class AppMediaViewer {
div.append(video);
}
const player = new VideoPlayer(video, true, streamable);
const player = new VideoPlayer(video, true, media.supportsStreaming);
return player;
/* player.wrapper.parentElement.append(video);
mover.append(player.wrapper); */
@ -827,61 +826,20 @@ export class AppMediaViewer { @@ -827,61 +826,20 @@ export class AppMediaViewer {
if(!source.src || (media.url && media.url != source.src)) {
const load = () => {
const promise = appDocsManager.downloadDoc(media.id);
const streamable = media.supportsStreaming && !media.url;
//if(!streamable) {
this.preloader.attach(mover, true, promise);
//}
let player: VideoPlayer;
let offset = 0;
let loadedParts: {[offset: number]: true} = {};
let preloaderNotify = promise.notify;
let promiseNotify = (details: {offset: number, total: number, done: number}) => {
if(player) {
//player.progress.setLoadProgress(details.done / details.total);
setLoadProgress();
}
loadedParts[details.offset] = true;
preloaderNotify(details);
};
if(streamable) {
promise.notify = promiseNotify;
}
let setLoadProgress = () => {
let rounded = offset - (offset % 524288);
let downloadedAfter = 0;
for(let i in loadedParts) {
let o = +i;
if(o >= rounded) {
downloadedAfter += 524288;
}
}
if(offset > rounded) {
downloadedAfter -= offset % 524288;
}
player.progress.setLoadProgress(Math.min(1, downloadedAfter / media.size + rounded / media.size));
};
promise.then(async(mp4Source: any) => {
promise.then(async() => {
if(this.currentMessageID != message.mid) {
this.log.warn('media viewer changed video');
return;
}
const isStream = mp4Source instanceof MP4Source;
if(isStream) {
promise.notify = promiseNotify;
}
const url = isStream ? mp4Source.getURL() : media.url;
const url = media.url;
if(target instanceof SVGSVGElement && (video.parentElement || !isSafari)) { // if video exists
if(!video.parentElement) {
div.firstElementChild.lastElementChild.append(video);
@ -914,16 +872,7 @@ export class AppMediaViewer { @@ -914,16 +872,7 @@ export class AppMediaViewer {
});
});
player = createPlayer(streamable);
if(player && mp4Source instanceof MP4Source) {
player.progress.onSeek = (time) => {
//this.log('seek', time);
offset = mp4Source.seek(time);
setLoadProgress();
};
this.log('lol');
}
player = createPlayer();
});
return promise;

86
src/lib/appManagers/appPhotosManager.ts

@ -1,10 +1,10 @@ @@ -1,10 +1,10 @@
import appUsersManager from "./appUsersManager";
import { calcImageInBox, isObject, getFileURL } from "../utils";
import fileManager from '../filemanager';
import { bytesFromHex } from "../bin_utils";
import { bytesFromHex, getFileNameByLocation } from "../bin_utils";
//import apiManager from '../mtproto/apiManager';
import apiManager from '../mtproto/mtprotoworker';
import { MTPhotoSize, inputPhotoFileLocation, inputDocumentFileLocation, InputFileLocation, FileLocation } from "../../types";
import { MTPhotoSize, inputPhotoFileLocation, inputDocumentFileLocation, FileLocation } from "../../types";
import appDownloadManager from "./appDownloadManager";
export type MTPhoto = {
_: 'photo' | 'photoEmpty' | string,
@ -58,38 +58,6 @@ export class AppPhotosManager { @@ -58,38 +58,6 @@ export class AppPhotosManager {
if(!photo.id) {
console.warn('no apiPhoto.id', photo);
} else this.photos[photo.id] = photo as any;
/* if(!('sizes' in photo)) return;
photo.sizes.forEach((photoSize: any) => {
if(photoSize._ == 'photoCachedSize') {
apiFileManager.saveSmallFile(photoSize.location, photoSize.bytes);
console.log('clearing photo cached size', photo);
// Memory
photoSize.size = photoSize.bytes.length;
delete photoSize.bytes;
photoSize._ = 'photoSize';
}
}); */
/* if(!photo.downloaded) {
photo.downloaded = apiFileManager.isFileExists({
_: 'inputPhotoFileLocation',
id: photo.id,
access_hash: photo.access_hash,
file_reference: photo.file_reference
});
// apiFileManager.isFileExists({
// _: 'inputPhotoFileLocation',
// id: photo.id,
// access_hash: photo.access_hash,
// file_reference: photo.file_reference
// }).then(downloaded => {
// photo.downloaded = downloaded;
// });
} */
return photo;
}
@ -326,52 +294,22 @@ export class AppPhotosManager { @@ -326,52 +294,22 @@ export class AppPhotosManager {
}
public downloadPhoto(photoID: string) {
var photo = this.photos[photoID];
var ext = 'jpg';
var mimeType = 'image/jpeg';
var fileName = 'photo' + photoID + '.' + ext;
var fullWidth = this.windowW;
var fullHeight = this.windowH;
var fullPhotoSize = this.choosePhotoSize(photo, fullWidth, fullHeight);
var inputFileLocation: inputDocumentFileLocation | inputPhotoFileLocation = {
// @ts-ignore
const photo = this.photos[photoID];
const fullWidth = this.windowW;
const fullHeight = this.windowH;
const fullPhotoSize = this.choosePhotoSize(photo, fullWidth, fullHeight);
const location: inputDocumentFileLocation | inputPhotoFileLocation = {
_: photo._ == 'document' ? 'inputDocumentFileLocation' : 'inputPhotoFileLocation',
id: photo.id,
access_hash: photo.access_hash,
file_reference: photo.file_reference,
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
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);
});
}
appDownloadManager.downloadToDisc(fileName, url);
}
}

128
src/lib/filemanager.ts

@ -1,12 +1,5 @@ @@ -1,12 +1,5 @@
import {blobConstruct} from './bin_utils';
/* import 'web-streams-polyfill/ponyfill';
// @ts-ignore
import streamSaver from 'streamsaver';
if(window.location.href.indexOf('localhost') === -1) {
streamSaver.mitm = 'mitm.html';
} */
class FileManager {
public blobSupported = true;
@ -21,21 +14,7 @@ class FileManager { @@ -21,21 +14,7 @@ class FileManager {
public isAvailable() {
return this.blobSupported;
}
/* public copy(fromFileEntry: any, toFileEntry: any) {
return this.getFileWriter(toFileEntry).then((fileWriter) => {
return this.write(fileWriter, fromFileEntry).then(() => {
return fileWriter;
}, (error: any) => {
try {
// @ts-ignore
fileWriter.truncate(0);
} catch (e) {}
return Promise.reject(error);
});
});
} */
public copy(fromFileEntry: any, toFileEntry: any) {
return this.write(toFileEntry, fromFileEntry).then(() => {
console.log('copy success');
@ -52,32 +31,6 @@ class FileManager { @@ -52,32 +31,6 @@ class FileManager {
});
}
/* public write(fileWriter: any, bytes: any) {
return new Promise((resolve, reject) => {
fileWriter.onwriteend = function(e: any) {
resolve();
};
fileWriter.onerror = function(e: any) {
reject(e);
};
if(bytes.file) {
bytes.file((file: any) => {
fileWriter.write(file);
}, reject);
} else if(bytes instanceof Blob) { // is file bytes
fileWriter.write(bytes);
} else {
try {
var blob = blobConstruct([bytesToArrayBuffer(bytes)]);
fileWriter.write(blob);
} catch(e) {
reject(e);
}
}
});
} */
public write(fileWriter: ReturnType<FileManager['getFakeFileWriter']>, bytes: Uint8Array | Blob | {file: any}): Promise<void> {
if('file' in bytes) {
return bytes.file((file: any) => {
@ -97,23 +50,10 @@ class FileManager { @@ -97,23 +50,10 @@ class FileManager {
fileReader.readAsArrayBuffer(bytes);
});
} else {
//var blob = blobConstruct([bytesToArrayBuffer(bytes)]);
//return fileWriter.write(blob);
return fileWriter.write(bytes);
}
}
public chooseSaveFile(fileName: string, ext: string, mimeType: string, size?: number): any {
throw new Error('no writer');
/* let fileStream = streamSaver.createWriteStream(fileName, {
size: size,
writableStrategy: undefined,
readableStrategy: undefined
});
let writer = fileStream.getWriter();
return writer; */
}
public getFakeFileWriter(mimeType: string, saveFileCallback: (blob: Blob) => Promise<Blob>) {
let blobParts: Array<Uint8Array> = [];
const fakeFileWriter = {
@ -139,70 +79,6 @@ class FileManager { @@ -139,70 +79,6 @@ class FileManager {
return fakeFileWriter;
}
public download(blob: Blob, mimeType: string, fileName: string) {
if(window.navigator && navigator.msSaveBlob !== undefined) {
window.navigator.msSaveBlob(blob, fileName);
return false;
}
if(window.navigator && 'getDeviceStorage' in navigator) {
var storageName = 'sdcard';
var subdir = 'telegram/';
switch(mimeType.split('/')[0]) {
case 'video':
storageName = 'videos';
break;
case 'audio':
storageName = 'music';
break;
case 'image':
storageName = 'pictures';
break;
}
// @ts-ignore
var deviceStorage = navigator.getDeviceStorage(storageName);
var request = deviceStorage.addNamed(blob, subdir + fileName);
request.onsuccess = function () {
console.log('Device storage save result', this.result);
};
request.onerror = () => {};
return;
}
let url = URL.createObjectURL(blob);
var anchor = document.createElementNS('http://www.w3.org/1999/xhtml', 'a') as HTMLAnchorElement;
anchor.href = url as string;
anchor.download = fileName;
if(anchor.dataset) {
anchor.dataset.downloadurl = ['video/quicktime', fileName, url].join(':');
}
anchor.style.position = 'absolute';
anchor.style.top = '1px';
anchor.style.left = '1px';
document.body.append(anchor);
try {
var clickEvent = document.createEvent('MouseEvents');
clickEvent.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
anchor.dispatchEvent(clickEvent);
} catch (e) {
console.error('Download click error', e);
try {
anchor.click();
} catch (e) {
window.open(url as string, '_blank');
}
}
setTimeout(() => {
anchor.remove();
}, 100);
}
}
export default new FileManager();

34
src/lib/libwebp.js

File diff suppressed because one or more lines are too long

461
src/lib/lottieLoader copy.ts

@ -1,461 +0,0 @@ @@ -1,461 +0,0 @@
import { isInDOM } from "./utils";
let convert = (value: number) => {
return Math.round(Math.min(Math.max(value, 0), 1) * 255);
};
type RLottiePlayerListeners = 'firstFrame' | 'enterFrame';
export class RLottiePlayer {
public static reqId = 0;
public reqId = 0;
public curFrame: number;
public worker: QueryableWorker;
public el: HTMLElement;
public width: number;
public height: number;
public listeners: Partial<{
[k in RLottiePlayerListeners]: (res: any) => void
}> = {};
public listenerResults: Partial<{
[k in RLottiePlayerListeners]: any
}> = {};
public canvas: HTMLCanvasElement;
public context: CanvasRenderingContext2D;
public paused = false;
public direction = 1;
public speed = 1;
public autoplay = true;
constructor({el, width, height, worker}: {
el: HTMLElement,
width: number,
height: number,
worker: QueryableWorker
}) {
this.reqId = ++RLottiePlayer['reqId'];
this.el = el;
this.width = width;
this.height = height;
this.worker = worker;
}
public addListener(name: RLottiePlayerListeners, callback: (res?: any) => void) {
if(this.listenerResults.hasOwnProperty(name)) return Promise.resolve(this.listenerResults[name]);
this.listeners[name] = callback;
}
public setListenerResult(name: RLottiePlayerListeners, value?: any) {
this.listenerResults[name] = value;
if(this.listeners[name]) {
this.listeners[name](value);
}
}
private sendQuery(methodName: string, ...args: any[]) {
this.worker.sendQuery(methodName, this.reqId, ...args);
}
public loadFromData(json: any) {
this.sendQuery('loadFromData', json, this.width, this.height, {
paused: this.paused,
direction: this.direction,
speed: this.speed
});
}
public play() {
this.sendQuery('play');
this.paused = false;
}
public pause() {
this.sendQuery('pause');
this.paused = true;
}
public stop() {
this.sendQuery('stop');
this.paused = true;
}
public restart() {
this.sendQuery('restart');
}
public setSpeed(speed: number) {
this.sendQuery('setSpeed', speed);
}
public setDirection(direction: number) {
this.direction = direction;
this.sendQuery('setDirection', direction);
}
public destroy() {
lottieLoader.onDestroy(this.reqId);
this.sendQuery('destroy');
}
private attachPlayer() {
this.canvas = document.createElement('canvas');
this.canvas.width = this.width;
this.canvas.height = this.height;
//this.el.appendChild(this.canvas);
this.context = this.canvas.getContext('2d');
}
public renderFrame(frame: Uint8ClampedArray, frameNo: number) {
if(!this.listenerResults.hasOwnProperty('firstFrame')) {
this.attachPlayer();
this.el.appendChild(this.canvas);
this.setListenerResult('firstFrame');
}
this.context.putImageData(new ImageData(frame, this.width, this.height), 0, 0);
this.setListenerResult('enterFrame', frameNo);
}
}
class QueryableWorker {
private worker: Worker;
private listeners: {[name: string]: (...args: any[]) => void} = {};
constructor(url: string, private defaultListener: (data: any) => void = () => {}, onError?: (error: any) => void) {
this.worker = new Worker(url);
if(onError) {
this.worker.onerror = onError;
}
this.worker.onmessage = (event) => {
if(event.data instanceof Object &&
event.data.hasOwnProperty('queryMethodListener') &&
event.data.hasOwnProperty('queryMethodArguments')) {
this.listeners[event.data.queryMethodListener].apply(this, event.data.queryMethodArguments);
} else {
this.defaultListener.call(this, event.data);
}
}
}
public postMessage(message: any) {
this.worker.postMessage(message);
}
public terminate() {
this.worker.terminate();
}
public addListener(name: string, listener: (...args: any[]) => void) {
this.listeners[name] = listener;
}
public removeListener(name: string) {
delete this.listeners[name];
}
public sendQuery(queryMethod: string, ...args: any[]) {
this.worker.postMessage({
'queryMethod': queryMethod,
'queryMethodArguments': args
});
}
}
class LottieLoader {
public loadPromise: Promise<void>;
public loaded = false;
private static COLORREPLACEMENTS = [
[
[0xf77e41, 0xca907a],
[0xffb139, 0xedc5a5],
[0xffd140, 0xf7e3c3],
[0xffdf79, 0xfbefd6],
],
[
[0xf77e41, 0xaa7c60],
[0xffb139, 0xc8a987],
[0xffd140, 0xddc89f],
[0xffdf79, 0xe6d6b2],
],
[
[0xf77e41, 0x8c6148],
[0xffb139, 0xad8562],
[0xffd140, 0xc49e76],
[0xffdf79, 0xd4b188],
],
[
[0xf77e41, 0x6e3c2c],
[0xffb139, 0x925a34],
[0xffd140, 0xa16e46],
[0xffdf79, 0xac7a52],
]
];
private workersLimit = 4;
private players: {[reqId: number]: RLottiePlayer} = {};
private byGroups: {[group: string]: RLottiePlayer[]} = {};
private workers: QueryableWorker[] = [];
private curWorkerNum = 0;
private observer: IntersectionObserver;
private visible: Set<RLottiePlayer> = new Set();
private debug = true;
constructor() {
this.observer = new IntersectionObserver((entries) => {
for(const entry of entries) {
const target = entry.target;
for(const group in this.byGroups) {
const player = this.byGroups[group].find(p => p.el == target);
if(player) {
if(entry.isIntersecting) {
this.visible.add(player);
if(player.paused) {
player.play();
}
} else {
this.visible.delete(player);
if(!player.paused) {
player.pause();
}
}
break;
}
}
}
});
}
public loadLottieWorkers() {
if(this.loadPromise) return this.loadPromise;
const onFrame = this.onFrame.bind(this);
return this.loadPromise = new Promise((resolve, reject) => {
let remain = this.workersLimit;
for(let i = 0; i < this.workersLimit; ++i) {
const worker = this.workers[i] = new QueryableWorker('rlottie.worker.js');
worker.addListener('ready', () => {
console.log('worker #' + i + ' ready');
worker.addListener('frame', onFrame);
--remain;
if(!remain) {
console.log('workers ready');
resolve();
this.loaded = true;
}
});
}
});
}
private applyReplacements(object: any, toneIndex: number) {
const replacements = LottieLoader.COLORREPLACEMENTS[toneIndex - 2];
const iterateIt = (it: any) => {
for(let smth of it) {
switch(smth.ty) {
case 'st':
case 'fl':
let k = smth.c.k;
let color = convert(k[2]) | (convert(k[1]) << 8) | (convert(k[0]) << 16);
let foundReplacement = replacements.find(p => p[0] == color);
if(foundReplacement) {
k[0] = ((foundReplacement[1] >> 16) & 255) / 255;
k[1] = ((foundReplacement[1] >> 8) & 255) / 255;
k[2] = (foundReplacement[1] & 255) / 255;
}
//console.log('foundReplacement!', foundReplacement, color.toString(16), k);
break;
}
if(smth.hasOwnProperty('it')) {
iterateIt(smth.it);
}
}
};
for(let layer of object.layers) {
if(!layer.shapes) continue;
for(let shape of layer.shapes) {
iterateIt(shape.it);
}
}
}
public async loadAnimationWorker(params: {
container: HTMLElement,
autoplay?: boolean,
animationData: any,
loop?: boolean,
renderer?: string,
width?: number,
height?: number
}, group = '', toneIndex = -1) {
//params.autoplay = false;
if(toneIndex >= 1 && toneIndex <= 5) {
this.applyReplacements(params.animationData, toneIndex);
}
if(!this.loaded) {
await this.loadLottieWorkers();
}
this.observer.observe(params.container);
const width = params.width || parseInt(params.container.style.width);
const height = params.height || parseInt(params.container.style.height);
const player = this.initPlayer(params.container, params.animationData, width, height);
for(let i in params) {
// @ts-ignore
if(player.hasOwnProperty(i)) {
// @ts-ignore
player[i] = params[i];
}
}
(this.byGroups[group] ?? (this.byGroups[group] = [])).push(player);
return player;
}
public checkAnimations(blurred?: boolean, group?: string, destroy = false) {
const groups = group && false ? [group] : Object.keys(this.byGroups);
if(group && !this.byGroups[group]) {
console.warn('no animation group:', group);
this.byGroups[group] = [];
//return;
}
for(const group of groups) {
const animations = this.byGroups[group];
const length = animations.length;
for(let i = length - 1; i >= 0; --i) {
const player = animations[i];
if(destroy || (!isInDOM(player.el) && player.listenerResults.hasOwnProperty('firstFrame'))) {
//console.log('destroy animation');
player.destroy();
continue;
}
if(blurred) {
if(!player.paused) {
this.debug && console.log('pause animation', player);
player.pause();
}
} else if(player.paused && this.visible.has(player)) {
this.debug && console.log('play animation', player);
player.play();
}
/* if(canvas) {
let c = container.firstElementChild as HTMLCanvasElement;
if(!c) {
console.warn('no canvas element for check!', container, animations[i]);
continue;
}
if(!c.height && !c.width && isElementInViewport(container)) {
//console.log('lottie need resize');
animation.resize();
}
} */
//if(!autoplay) continue;
/* if(blurred || !isElementInViewport(container)) {
if(!paused) {
this.debug && console.log('pause animation', isElementInViewport(container), container);
animation.pause();
animations[i].paused = true;
}
} else if(paused) {
this.debug && console.log('play animation', container);
animation.play();
animations[i].paused = false;
} */
}
}
}
private onFrame(reqId: number, frameNo: number, frame: Uint8ClampedArray, width: number, height: number) {
const rlPlayer = this.players[reqId];
if(!rlPlayer) {
this.debug && console.warn('onFrame on destroyed player:', reqId, frameNo);
return;
}
rlPlayer.renderFrame(frame, frameNo);
}
public onDestroy(reqId: number) {
let player = this.players[reqId];
for(let group in this.byGroups) {
this.byGroups[group].findAndSplice(p => p == player);
}
delete this.players[player.reqId];
this.observer.unobserve(player.el);
this.visible.delete(player);
}
public destroyWorkers() {
this.workers.forEach((worker, idx) => {
worker.terminate();
console.log('worker #' + idx + ' terminated');
});
console.log('workers destroyed');
this.workers.length = 0;
}
private initPlayer(el: HTMLElement, json: any, width: number, height: number) {
const rlPlayer = new RLottiePlayer({
el,
width,
height,
worker: this.workers[this.curWorkerNum++]
});
this.players[rlPlayer.reqId] = rlPlayer;
if(this.curWorkerNum >= this.workers.length) {
this.curWorkerNum = 0;
}
rlPlayer.loadFromData(json);
return rlPlayer;
}
}
const lottieLoader = new LottieLoader();
(window as any).LottieLoader = lottieLoader;
export default lottieLoader;

33
src/lib/mediaPlayer.ts

@ -11,7 +11,7 @@ export class MediaProgressLine { @@ -11,7 +11,7 @@ export class MediaProgressLine {
public onSeek: (time: number) => void;
constructor(private media: HTMLAudioElement | HTMLVideoElement, streamable = false) {
constructor(private media: HTMLAudioElement | HTMLVideoElement, private streamable = false) {
this.container = document.createElement('div');
this.container.classList.add('media-progress');
@ -22,6 +22,7 @@ export class MediaProgressLine { @@ -22,6 +22,7 @@ export class MediaProgressLine {
this.filledLoad = document.createElement('div');
this.filledLoad.classList.add('media-progress__filled', 'media-progress__loaded');
this.container.append(this.filledLoad);
//this.setLoadProgress();
}
let seek = this.seek = document.createElement('input');
@ -62,6 +63,10 @@ export class MediaProgressLine { @@ -62,6 +63,10 @@ export class MediaProgressLine {
window.cancelAnimationFrame(this.progressRAF);
}
if(this.streamable) {
this.setLoadProgress();
}
this.progressRAF = window.requestAnimationFrame(r);
};
@ -96,7 +101,29 @@ export class MediaProgressLine { @@ -96,7 +101,29 @@ export class MediaProgressLine {
this.mousedown = false;
};
public setLoadProgress(percents: number) {
onProgress = (e: Event) => {
this.setLoadProgress();
};
private setLoadProgress() {
const buf = this.media.buffered;
const numRanges = buf.length;
const currentTime = this.media.currentTime;
let nearestStart = 0, end = 0;
for(let i = 0; i < numRanges; ++i) {
const start = buf.start(i);
if(currentTime >= start && start >= nearestStart) {
nearestStart = start;
end = buf.end(i);
}
//console.log('onProgress range:', i, buf.start(i), buf.end(i), this.media);
}
//console.log('onProgress correct range:', nearestStart, end, this.media);
const percents = this.media.duration ? end / this.media.duration : 0;
this.filledLoad.style.transform = 'scaleX(' + percents + ')';
}
@ -120,6 +147,7 @@ export class MediaProgressLine { @@ -120,6 +147,7 @@ export class MediaProgressLine {
private setListeners() {
this.media.addEventListener('ended', this.onEnded);
this.media.addEventListener('play', this.onPlay);
this.streamable && this.media.addEventListener('progress', this.onProgress);
this.container.addEventListener('mousemove', this.onMouseMove);
this.container.addEventListener('mousedown', this.onMouseDown);
@ -143,6 +171,7 @@ export class MediaProgressLine { @@ -143,6 +171,7 @@ export class MediaProgressLine {
this.media.removeEventListener('loadeddata', this.onLoadedData);
this.media.removeEventListener('ended', this.onEnded);
this.media.removeEventListener('play', this.onPlay);
this.streamable && this.media.removeEventListener('progress', this.onProgress);
this.container.removeEventListener('mousemove', this.onMouseMove);
this.container.removeEventListener('mousedown', this.onMouseDown);

183
src/lib/mtproto/apiFileManager.ts

@ -6,7 +6,7 @@ import apiManager from "./apiManager"; @@ -6,7 +6,7 @@ import apiManager from "./apiManager";
import { deferredPromise, CancellablePromise } from "../polyfill";
import appWebpManager from "../appManagers/appWebpManager";
import { logger } from "../logger";
import { InputFileLocation, FileLocation } from "../../types";
import { InputFileLocation, FileLocation, UploadFile } from "../../types";
type Delayed = {
offset: number,
@ -14,18 +14,25 @@ type Delayed = { @@ -14,18 +14,25 @@ type Delayed = {
writeFileDeferred: CancellablePromise<unknown>
};
type DownloadOptions = Partial<{
}>;
export type DownloadOptions = {
dcID: number,
location: InputFileLocation | FileLocation,
size: number,
fileName?: string,
mimeType?: string,
limitPart?: number,
stickerType?: number,
processPart?: (bytes: Uint8Array, offset?: number, queue?: Delayed[]) => Promise<any>
};
export class ApiFileManager {
public cachedDownloadPromises: {
[fileName: string]: Promise<Blob>
[fileName: string]: CancellablePromise<Blob>
} = {};
public downloadPulls: {
[x: string]: Array<{
cb: () => Promise<unknown>,
cb: () => Promise<UploadFile | void>,
deferred: {
resolve: (...args: any[]) => void,
reject: (...args: any[]) => void
@ -37,7 +44,9 @@ export class ApiFileManager { @@ -37,7 +44,9 @@ export class ApiFileManager {
private log: ReturnType<typeof logger> = logger('AFM');
public downloadRequest(dcID: string | number, cb: () => Promise<unknown>, activeDelta: number) {
public downloadRequest(dcID: 'upload', cb: () => Promise<void>, activeDelta: number): Promise<void>;
public downloadRequest(dcID: number, cb: () => Promise<UploadFile>, activeDelta: number): Promise<UploadFile>;
public downloadRequest(dcID: number | string, cb: () => Promise<UploadFile | void>, activeDelta: number) {
if(this.downloadPulls[dcID] === undefined) {
this.downloadPulls[dcID] = [];
this.downloadActives[dcID] = 0;
@ -45,9 +54,9 @@ export class ApiFileManager { @@ -45,9 +54,9 @@ export class ApiFileManager {
const downloadPull = this.downloadPulls[dcID];
const promise = new Promise((resolve, reject) => {
downloadPull.push({cb: cb, deferred: {resolve, reject}, activeDelta: activeDelta});
})/* .catch(() => {}) */;
const promise = new Promise<UploadFile | void>((resolve, reject) => {
downloadPull.push({cb, deferred: {resolve, reject}, activeDelta});
});
setTimeout(() => {
this.downloadCheck(dcID);
@ -60,7 +69,7 @@ export class ApiFileManager { @@ -60,7 +69,7 @@ export class ApiFileManager {
const downloadPull = this.downloadPulls[dcID];
//const downloadLimit = dcID == 'upload' ? 11 : 5;
//const downloadLimit = 24;
const downloadLimit = dcID == 'upload' ? 11 : 24;
const downloadLimit = dcID == 'upload' ? 11 : 50;
if(this.downloadActives[dcID] >= downloadLimit || !downloadPull || !downloadPull.length) {
return false;
@ -72,12 +81,12 @@ export class ApiFileManager { @@ -72,12 +81,12 @@ export class ApiFileManager {
this.downloadActives[dcID] += activeDelta;
data.cb()
.then((result: any) => {
.then((result) => {
this.downloadActives[dcID] -= activeDelta;
this.downloadCheck(dcID);
data.deferred.resolve(result);
}, (error: any) => {
}, (error: Error) => {
if(error) {
this.log.error('downloadCheck error:', error);
}
@ -93,16 +102,48 @@ export class ApiFileManager { @@ -93,16 +102,48 @@ export class ApiFileManager {
return cacheStorage;
}
public downloadFile(options: {
dcID: number,
location: InputFileLocation | FileLocation,
size: number,
mimeType?: string,
toFileEntry?: any,
limitPart?: number,
stickerType?: number,
processPart?: (bytes: Uint8Array, offset: number, queue: Delayed[]) => Promise<any>
}): CancellablePromise<Blob> {
public cancelDownload(fileName: string) {
const promise = this.cachedDownloadPromises[fileName];
if(promise) {
promise.cancel();
return true;
}
return false;
}
public requestFilePart(dcID: number, location: InputFileLocation | FileLocation, offset: number, limit: number, checkCancel?: () => void) {
const delta = limit / 1024 / 256;
return this.downloadRequest(dcID, async() => {
checkCancel && checkCancel();
return apiManager.invokeApi('upload.getFile', {
location,
offset,
limit
}, {
dcID,
fileDownload: true/* ,
singleInRequest: 'safari' in window */
}) as Promise<UploadFile>;
}, delta);
}
private convertBlobToBytes(blob: Blob) {
return blob.arrayBuffer().then(buffer => new Uint8Array(buffer));
}
private getLimitPart(size: number): number {
let bytes: number;
if(size < 1e6 || !size) bytes = 512;
else if(size < 3e6) bytes = 256;
else bytes = 128;
return bytes * 1024;
}
public downloadFile(options: DownloadOptions): CancellablePromise<Blob> {
if(!FileManager.isAvailable()) {
return Promise.reject({type: 'BROWSER_BLOB_NOT_SUPPORTED'});
}
@ -112,7 +153,7 @@ export class ApiFileManager { @@ -112,7 +153,7 @@ export class ApiFileManager {
let processSticker = false;
if(options.stickerType == 1 && !appWebpManager.isSupported()) {
if(options.toFileEntry || size > 524288) {
if(size > 524288) {
delete options.stickerType;
} else {
processSticker = true;
@ -122,16 +163,18 @@ export class ApiFileManager { @@ -122,16 +163,18 @@ export class ApiFileManager {
// this.log('Dload file', dcID, location, size)
const fileName = getFileNameByLocation(location);
const toFileEntry = options.toFileEntry || null;
const cachedPromise = this.cachedDownloadPromises[fileName];
const fileStorage = this.getFileStorage();
//this.log('downloadFile', fileName, fileName.length, location, arguments);
if(cachedPromise) {
if(toFileEntry) {
return cachedPromise.then((blob: any) => {
return FileManager.copy(blob, toFileEntry);
if(options.processPart) {
return cachedPromise.then((blob) => {
return this.convertBlobToBytes(blob).then(bytes => {
options.processPart(bytes)
return blob;
});
});
}
@ -181,30 +224,28 @@ export class ApiFileManager { @@ -181,30 +224,28 @@ export class ApiFileManager {
throw false;
}
if(toFileEntry) {
FileManager.copy(blob, toFileEntry).then(deferred.resolve, errorHandler);
} else {
deferred.resolve(blob);
if(options.processPart) {
//FileManager.copy(blob, toFileEntry).then(deferred.resolve, errorHandler);
await this.convertBlobToBytes(blob).then(bytes => {
options.processPart(bytes);
});
}
deferred.resolve(blob);
}).catch(() => {
//this.log('not cached', fileName);
//var fileWriterPromise = toFileEntry ? FileManager.getFileWriter(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType);
const fileWriterPromise = toFileEntry ? Promise.resolve(toFileEntry) : fileStorage.getFileWriter(fileName, mimeType);
const fileWriterPromise = fileStorage.getFileWriter(fileName, mimeType);
fileWriterPromise.then((fileWriter: any) => {
fileWriterPromise.then((fileWriter) => {
cacheFileWriter = fileWriter;
const limit = options.limitPart || 524288;
const limit = options.limitPart || this.getLimitPart(size);
let offset: number;
let startOffset = 0;
let writeFilePromise: CancellablePromise<unknown> = Promise.resolve(),
writeFileDeferred: CancellablePromise<unknown>;
const maxRequests = options.processPart ? 5 : 5;
const maxRequests = options.processPart ? 5 : 10;
if(!size) {
size = limit;
}
if(fileWriter.length) {
/* if(fileWriter.length) {
startOffset = fileWriter.length;
if(startOffset >= size) {
@ -221,7 +262,7 @@ export class ApiFileManager { @@ -221,7 +262,7 @@ export class ApiFileManager {
deferred.notify({done: startOffset, total: size});
/////this.log('deferred notify 1:', {done: startOffset, total: size});
}
} */
const processDownloaded = async(bytes: Uint8Array, offset: number) => {
if(options.processPart) {
@ -236,18 +277,20 @@ export class ApiFileManager { @@ -236,18 +277,20 @@ export class ApiFileManager {
};
const delayed: Delayed[] = [];
for(offset = startOffset; offset < size; offset += limit) {
offset = startOffset;
do {
////this.log('offset:', startOffset);
writeFileDeferred = deferredPromise<void>();
delayed.push({offset, writeFilePromise, writeFileDeferred});
writeFilePromise = writeFileDeferred;
////this.log('offset:', startOffset);
}
offset += limit;
} while(offset < size);
// для потокового видео нужно скачать первый и последний чанки
if(options.processPart && delayed.length > 2) {
/* if(options.processPart && delayed.length > 2) {
const last = delayed.splice(delayed.length - 1, 1)[0];
delayed.splice(1, 0, last);
}
} */
// @ts-ignore
//deferred.queue = delayed;
@ -258,21 +301,7 @@ export class ApiFileManager { @@ -258,21 +301,7 @@ export class ApiFileManager {
const {offset, writeFilePromise, writeFileDeferred} = delayed.shift();
try {
const result: any = await this.downloadRequest(dcID, () => {
if(canceled) {
return Promise.resolve();
}
return apiManager.invokeApi('upload.getFile', {
location,
offset,
limit
}, {
dcID,
fileDownload: true/* ,
singleInRequest: 'safari' in window */
});
}, 2);
const result = await this.requestFilePart(dcID, location, offset, limit, checkCancel);
if(delayed.length) {
superpuper();
@ -280,11 +309,10 @@ export class ApiFileManager { @@ -280,11 +309,10 @@ export class ApiFileManager {
//////////////////////////////////////////
const processedResult = await processDownloaded(result.bytes, offset);
if(canceled) {
return Promise.resolve();
}
checkCancel();
done += limit;
//done += limit;
done += processedResult.byteLength;
const isFinal = offset + limit >= size;
//if(!isFinal) {
////this.log('deferred notify 2:', {done: offset + limit, total: size}, deferred);
@ -292,9 +320,7 @@ export class ApiFileManager { @@ -292,9 +320,7 @@ export class ApiFileManager {
//}
await writeFilePromise;
if(canceled) {
return Promise.resolve();
}
checkCancel();
await FileManager.write(fileWriter, processedResult);
writeFileDeferred.resolve();
@ -302,7 +328,7 @@ export class ApiFileManager { @@ -302,7 +328,7 @@ export class ApiFileManager {
if(isFinal) {
resolved = true;
if(toFileEntry) {
if(options.processPart) {
deferred.resolve();
} else {
deferred.resolve(fileWriter.finalize());
@ -319,22 +345,21 @@ export class ApiFileManager { @@ -319,22 +345,21 @@ export class ApiFileManager {
});
});
const checkCancel = () => {
if(canceled) {
throw new Error('canceled');
}
};
deferred.cancel = () => {
if(!canceled && !resolved) {
canceled = true;
delete this.cachedDownloadPromises[fileName];
errorHandler({type: 'DOWNLOAD_CANCELED'});
if(toFileEntry) {
toFileEntry.abort();
}
}
};
//console.log(deferred, deferred.notify, deferred.cancel);
if(!toFileEntry) {
this.cachedDownloadPromises[fileName] = deferred;
}
this.cachedDownloadPromises[fileName] = deferred;
return deferred;
}

202
src/lib/mtproto/mtproto.service.ts

@ -6,7 +6,11 @@ import apiManager from "./apiManager"; @@ -6,7 +6,11 @@ import apiManager from "./apiManager";
import AppStorage from '../storage';
import cryptoWorker from "../crypto/cryptoworker";
import networkerFactory from "./networkerFactory";
import apiFileManager from './apiFileManager';
import apiFileManager, { DownloadOptions } from './apiFileManager';
import { getFileNameByLocation } from '../bin_utils';
import { logger, LogLevels } from '../logger';
const log = logger('SW'/* , LogLevels.error */);
const ctx = self as any as ServiceWorkerGlobalScope;
@ -75,11 +79,10 @@ function respond(client: Client | ServiceWorker | MessagePort, ...args: any[]) { @@ -75,11 +79,10 @@ function respond(client: Client | ServiceWorker | MessagePort, ...args: any[]) {
} */
}
networkerFactory.setUpdatesProcessor((obj, bool) => {
//console.log('updatesss');
//ctx.postMessage({update: {obj, bool}});
//respond({update: {obj, bool}});
/**
* Broadcast Notification
*/
function notify(...args: any[]) {
ctx.clients.matchAll({ includeUncontrolled: false, type: 'window' }).then((listeners) => {
if(!listeners.length) {
//console.trace('no listeners?', self, listeners);
@ -87,15 +90,20 @@ networkerFactory.setUpdatesProcessor((obj, bool) => { @@ -87,15 +90,20 @@ networkerFactory.setUpdatesProcessor((obj, bool) => {
}
listeners.forEach(listener => {
listener.postMessage({update: {obj, bool}});
// @ts-ignore
listener.postMessage(...args);
});
});
}
networkerFactory.setUpdatesProcessor((obj, bool) => {
notify({update: {obj, bool}});
});
ctx.addEventListener('message', async(e) => {
const taskID = e.data.taskID;
//console.log('[SW] Got message:', taskID, e, e.data);
log.debug('got message:', taskID, e, e.data);
if(e.data.useLs) {
AppStorage.finishTask(e.data.taskID, e.data.args);
@ -110,6 +118,7 @@ ctx.addEventListener('message', async(e) => { @@ -110,6 +118,7 @@ ctx.addEventListener('message', async(e) => {
respond(e.source, {taskID: taskID, result: result});
});
case 'cancelDownload':
case 'downloadFile': {
/* // @ts-ignore
return apiFileManager.downloadFile(...e.data.args); */
@ -151,7 +160,7 @@ ctx.addEventListener('message', async(e) => { @@ -151,7 +160,7 @@ ctx.addEventListener('message', async(e) => {
* Service Worker Installation
*/
ctx.addEventListener('install', (event: ExtendableEvent) => {
//console.log('service worker is installing');
log('installing');
/* initCache();
@ -165,7 +174,7 @@ ctx.addEventListener('install', (event: ExtendableEvent) => { @@ -165,7 +174,7 @@ ctx.addEventListener('install', (event: ExtendableEvent) => {
* Service Worker Activation
*/
ctx.addEventListener('activate', (event) => {
//console.log('service worker activating', ctx);
log('activating', ctx);
/* if (!ctx.cache) initCache();
if (!ctx.network) initNetwork(); */
@ -184,28 +193,168 @@ function timeout(delay: number): Promise<Response> { @@ -184,28 +193,168 @@ function timeout(delay: number): Promise<Response> {
}));
}
ctx.addEventListener('error', (error) => {
log.error('error:', error);
});
/**
* Fetch requests
*/
ctx.addEventListener('fetch', (event: FetchEvent): void => {
const [, url, scope, fileName] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || [];
const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || [];
//console.log('[SW] fetch:', event, event.request, url, scope, fileName);
log.debug('[fetch]:', event);
switch(scope) {
case 'download':
case 'thumb':
case 'document':
case 'photo': {
const info = JSON.parse(decodeURIComponent(fileName));
const info: DownloadOptions = JSON.parse(decodeURIComponent(params));
const fileName = getFileNameByLocation(info.location);
//console.log('[SW] fetch cachedDownloadPromises:', info/* apiFileManager.cachedDownloadPromises, apiFileManager.cachedDownloadPromises.hasOwnProperty(fileName) */);
/* event.request.signal.addEventListener('abort', (e) => {
console.log('[SW] user aborted request:', fileName);
cancellablePromise.cancel();
});
const promise = apiFileManager.downloadFile(info).then(b => new Response(b));
event.respondWith(promise);
event.request.signal.onabort = (e) => {
console.log('[SW] user aborted request:', fileName);
cancellablePromise.cancel();
};
if(fileName == '5452060085729624717') {
setInterval(() => {
console.log('[SW] request status:', fileName, event.request.signal.aborted);
}, 1000);
} */
const cancellablePromise = apiFileManager.downloadFile(info);
cancellablePromise.notify = (progress: {done: number, total: number, offset: number}) => {
notify({progress: {fileName, ...progress}});
};
log.debug('[fetch] file:', /* info, */fileName);
const promise = cancellablePromise.then(b => new Response(b));
event.respondWith(Promise.race([
timeout(45 * 1000),
promise
]));
break;
}
case 'stream': {
const [offset, end] = parseRange(event.request.headers.get('Range'));
const info: DownloadOptions = JSON.parse(decodeURIComponent(params));
//const fileName = getFileNameByLocation(info.location);
log.debug('[stream]', url, offset, end);
event.respondWith(new Promise((resolve, reject) => {
// safari workaround
if(offset === 0 && end === 1) {
resolve(new Response(new Uint8Array(2).buffer, {
status: 206,
statusText: 'Partial Content',
headers: {
'Accept-Ranges': 'bytes',
'Content-Range': `bytes 0-1/${info.size || '*'}`,
'Content-Length': '2',
'Content-Type': info.mimeType || 'video/mp4',
},
}));
return;
}
const limit = end && end < STREAM_CHUNK_UPPER_LIMIT ? alignLimit(end - offset + 1) : STREAM_CHUNK_UPPER_LIMIT;
const alignedOffset = alignOffset(offset, limit);
//log.debug('[stream] requestFilePart:', info.dcID, info.location, alignedOffset, limit);
apiFileManager.requestFilePart(info.dcID, info.location, alignedOffset, limit).then(result => {
let ab = result.bytes;
//log.debug('[stream] requestFilePart result:', result);
const headers: Record<string, string> = {
'Accept-Ranges': 'bytes',
'Content-Range': `bytes ${alignedOffset}-${alignedOffset + ab.byteLength - 1}/${info.size || '*'}`,
'Content-Length': `${ab.byteLength}`,
};
if(info.mimeType) headers['Content-Type'] = info.mimeType;
if(isSafari(ctx)) {
ab = ab.slice(offset - alignedOffset, end - alignedOffset + 1);
headers['Content-Range'] = `bytes ${offset}-${offset + ab.byteLength - 1}/${info.size || '*'}`;
headers['Content-Length'] = `${ab.byteLength}`;
}
resolve(new Response(ab, {
status: 206,
statusText: 'Partial Content',
headers,
}));
});
}));
break;
}
/* case 'download': {
const info: DownloadOptions = JSON.parse(decodeURIComponent(params));
const promise = new Promise<Response>((resolve) => {
const headers: Record<string, string> = {
'Content-Disposition': `attachment; filename="${info.fileName}"`,
};
if(info.size) headers['Content-Length'] = info.size.toString();
if(info.mimeType) headers['Content-Type'] = info.mimeType;
log('[download] file:', info);
const stream = new ReadableStream({
start(controller: ReadableStreamDefaultController) {
const limitPart = DOWNLOAD_CHUNK_LIMIT;
apiFileManager.downloadFile({
...info,
limitPart,
processPart: (bytes, offset) => {
log('[download] file processPart:', bytes, offset);
controller.enqueue(new Uint8Array(bytes));
const isFinal = offset + limitPart >= info.size;
if(isFinal) {
controller.close();
}
return Promise.resolve();
}
}).catch(err => {
log.error('[download] error:', err);
controller.error(err);
});
},
cancel() {
log.error('[download] file canceled:', info);
}
});
resolve(new Response(stream, {headers}));
});
event.respondWith(promise);
break;
} */
case 'upload': {
if(event.request.method == 'POST') {
event.respondWith(event.request.blob().then(blob => {
@ -273,3 +422,24 @@ ctx.addEventListener('fetch', (event: FetchEvent): void => { @@ -273,3 +422,24 @@ ctx.addEventListener('fetch', (event: FetchEvent): void => {
else event.respondWith(fetch(event.request.url)); */
}
});
const DOWNLOAD_CHUNK_LIMIT = 512 * 1024;
const STREAM_CHUNK_UPPER_LIMIT = 256 * 1024;
const SMALLEST_CHUNK_LIMIT = 256 * 4;
function parseRange(header: string): [number, number] {
if(!header) return [0, 0];
const [, chunks] = header.split('=');
const ranges = chunks.split(', ');
const [offset, end] = ranges[0].split('-');
return [+offset, +end || 0];
}
function alignOffset(offset: number, base = SMALLEST_CHUNK_LIMIT) {
return offset - (offset % base);
}
function alignLimit(limit: number) {
return 2 ** Math.ceil(Math.log(limit) / Math.log(2));
}

40
src/lib/mtproto/mtprotoworker.ts

@ -1,8 +1,9 @@ @@ -1,8 +1,9 @@
import {dT, isObject, $rootScope} from '../utils';
import {isObject, $rootScope} from '../utils';
import AppStorage from '../storage';
import CryptoWorkerMethods from '../crypto/crypto_methods';
import runtime from 'serviceworker-webpack-plugin/lib/runtime';
import { InputFileLocation, FileLocation } from '../../types';
import { logger } from '../logger';
type Task = {
taskID: number,
@ -10,14 +11,6 @@ type Task = { @@ -10,14 +11,6 @@ type Task = {
args: any[]
};
/* let pending: any[] = [];
function resendPending() {
if(navigator.serviceWorker.controller) {
for(let i = 0; i < pending.length; i++) navigator.serviceWorker.controller.postMessage(pending[i]);
pending = [];
}
} */
class ApiManagerProxy extends CryptoWorkerMethods {
private taskID = 0;
private awaiting: {
@ -28,30 +21,37 @@ class ApiManagerProxy extends CryptoWorkerMethods { @@ -28,30 +21,37 @@ class ApiManagerProxy extends CryptoWorkerMethods {
}
} = {} as any;
private pending: Array<Task> = [];
private debug = false;
public updatesProcessor: (obj: any, bool: boolean) => void = null;
private log = logger('API-PROXY');
constructor() {
super();
console.log(dT(), 'ApiManagerProxy constructor');
this.log('constructor');
/**
* Service worker
*/
runtime.register({ scope: '/' });
(runtime.register({ scope: '/' }) as Promise<ServiceWorkerRegistration>).then(registration => {
}, (err) => {
this.log.error('SW registration failed!', err);
});
navigator.serviceWorker.ready.then((registration) => {
console.info(dT(), 'ApiManagerProxy set SW');
this.log('set SW');
this.releasePending();
//registration.update();
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
console.warn(dT(), 'ApiManagerProxy controllerchange');
this.log.warn('controllerchange');
this.releasePending();
navigator.serviceWorker.controller.addEventListener('error', (e) => {
console.error('controller error:', e);
this.log.error('controller error:', e);
});
});
@ -72,6 +72,8 @@ class ApiManagerProxy extends CryptoWorkerMethods { @@ -72,6 +72,8 @@ class ApiManagerProxy extends CryptoWorkerMethods {
if(this.updatesProcessor) {
this.updatesProcessor(e.data.update.obj, e.data.update.bool);
}
} else if(e.data.progress) {
$rootScope.$broadcast('download_progress', e.data.progress);
} else {
this.finalizeTask(e.data.taskID, e.data.result, e.data.error);
}
@ -81,14 +83,14 @@ class ApiManagerProxy extends CryptoWorkerMethods { @@ -81,14 +83,14 @@ class ApiManagerProxy extends CryptoWorkerMethods {
private finalizeTask(taskID: number, result: any, error: any) {
let deferred = this.awaiting[taskID];
if(deferred !== undefined) {
this.debug && console.log(dT(), 'ApiManagerProxy done', deferred.taskName, result, error);
this.log.debug('done', deferred.taskName, result, error);
result === undefined ? deferred.reject(error) : deferred.resolve(result);
delete this.awaiting[taskID];
}
}
public performTaskWorker<T>(task: string, ...args: any[]) {
this.debug && console.log(dT(), 'ApiManagerProxy start', task, args);
this.log.debug('start', task, args);
return new Promise<T>((resolve, reject) => {
this.awaiting[this.taskID] = {resolve, reject, taskName: task};
@ -168,6 +170,10 @@ class ApiManagerProxy extends CryptoWorkerMethods { @@ -168,6 +170,10 @@ class ApiManagerProxy extends CryptoWorkerMethods {
}> = {}): Promise<Blob> {
return this.performTaskWorker('downloadFile', dcID, location, size, options);
}
public cancelDownload(fileName: string) {
return this.performTaskWorker('cancelDownload', fileName);
}
}
const apiManagerProxy = new ApiManagerProxy();

12
src/lib/polyfill.ts

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { bytesToHex, bytesFromHex, dT, bufferConcats } from "./bin_utils";
import { bytesToHex, bytesFromHex, bufferConcats } from "./bin_utils";
// @ts-ignore
import {SecureRandom} from 'jsbn';
@ -77,16 +77,16 @@ Array.prototype.findAndSplice = function<T>(verify: (value: T, index?: number, a @@ -77,16 +77,16 @@ Array.prototype.findAndSplice = function<T>(verify: (value: T, index?: number, a
};
String.prototype.toHHMMSS = function(leadZero = false) {
let sec_num = parseInt(this + '', 10);
let hours: any = Math.floor(sec_num / 3600);
const sec_num = parseInt(this + '', 10);
const hours = Math.floor(sec_num / 3600);
let minutes: any = Math.floor((sec_num - (hours * 3600)) / 60);
let seconds: any = sec_num - (hours * 3600) - (minutes * 60);
if(hours < 10) hours = "0" + hours;
if(hours) leadZero = true;
if(minutes < 10) minutes = leadZero ? "0" + minutes : minutes;
if(seconds < 10) seconds = "0" + seconds;
return minutes + ':' + seconds;
}
return (hours ? /* ('0' + hours).slice(-2) */hours + ':' : '') + minutes + ':' + seconds;
};
declare global {
interface Uint8Array {

24
src/lib/utils.ts

@ -151,8 +151,18 @@ export function getRichElementValue(node: any, lines: string[], line: string[], @@ -151,8 +151,18 @@ export function getRichElementValue(node: any, lines: string[], line: string[],
}
} */
type BroadcastKeys = 'download_progress' | 'user_update' | 'user_auth' | 'peer_changed' |
'filter_delete' | 'filter_update' | 'message_edit' | 'dialog_draft' | 'messages_pending' |
'history_append' | 'history_update' | 'dialogs_multiupdate' | 'dialog_unread' | 'dialog_flush' |
'dialog_drop' | 'dialog_migrate' | 'dialog_top' | 'history_reply_markup' | 'history_multiappend' |
'messages_read' | 'history_delete' | 'history_forbidden' | 'history_reload' | 'message_views' |
'message_sent' | 'history_request' | 'messages_downloaded' | 'contacts_update' | 'avatar_update' |
'stickers_installed' | 'stickers_deleted' | 'chat_full_update' | 'peer_pinned_message' |
'poll_update' | 'dialogs_archived_unread' | 'audio_play' | 'audio_pause' | 'chat_update' |
'apiUpdate' | 'stateSynchronized' | 'channel_settings' | 'webpage_updated' | 'draft_updated';
export const $rootScope = {
$broadcast: (name: string, detail?: any) => {
$broadcast: (name: BroadcastKeys, detail?: any) => {
if(name != 'user_update') {
console.debug(dT(), 'Broadcasting ' + name + ' event, with args:', detail);
}
@ -160,9 +170,14 @@ export const $rootScope = { @@ -160,9 +170,14 @@ export const $rootScope = {
let myCustomEvent = new CustomEvent(name, {detail});
document.dispatchEvent(myCustomEvent);
},
$on: (name: string, callback: any) => {
$on: (name: BroadcastKeys, callback: (e: CustomEvent) => any) => {
// @ts-ignore
document.addEventListener(name, callback);
},
$off: (name: BroadcastKeys, callback: (e: CustomEvent) => any) => {
// @ts-ignore
document.removeEventListener(name, callback);
},
selectedPeerID: 0,
myID: 0,
@ -523,11 +538,12 @@ export function getEmojiToneIndex(input: string) { @@ -523,11 +538,12 @@ export function getEmojiToneIndex(input: string) {
return match ? 5 - (57343 - match[0].charCodeAt(0)) : 0;
}
export function getFileURL(type: 'photo' | 'thumb' | 'document', options: {
export function getFileURL(type: 'photo' | 'thumb' | 'document' | 'stream' | 'download', options: {
dcID: number,
location: InputFileLocation | FileLocation,
size?: number,
mimeType?: string
mimeType?: string,
fileName?: string
}) {
//console.log('getFileURL', location);
//const perf = performance.now();

24
src/lib/webp_bad.js

@ -1,24 +0,0 @@ @@ -1,24 +0,0 @@
import insideWorker from 'offscreen-canvas/inside-worker';
console.log(self);
import { Webp } from "./libwebp.js";
let webp = new Webp();
webp.Module.doNotCaptureKeyboard = true;
webp.Module.noImageDecoding = true;
let canvas = null;
const worker = insideWorker(e => {
if(e.data.canvas) {
canvas = e.data.canvas;
console.log(e, canvas);
webp.setCanvas(canvas);
//webp.webpToSdl()
// Draw on the canvas
} else if(e.data.message == 'webpBytes') {
webp.webpToSdl(e.data.bytes, e.data.bytes.length);
//console.log(canvas);
self.postMessage({converted: true});
}
});

13
src/scss/partials/_ckin.scss

@ -184,10 +184,8 @@ html.no-touch .default.is-playing:hover .default__controls { @@ -184,10 +184,8 @@ html.no-touch .default.is-playing:hover .default__controls {
&__loaded {
background: rgba(255, 255, 255, 0.38);
position: absolute;
width: calc(100% - 16px);
left: 0;
top: 0;
left: 11px;
width: calc(100% - 11px);
}
}
}
@ -273,6 +271,13 @@ video::-webkit-media-controls-enclosure { @@ -273,6 +271,13 @@ video::-webkit-media-controls-enclosure {
cursor: pointer;
margin: 0;
}
&__loaded {
position: absolute;
left: 12px;
top: 0;
width: calc(100% - 12px);
}
}
input[type=range] {

11
src/types.d.ts vendored

@ -89,6 +89,17 @@ export type AccountPassword = { @@ -89,6 +89,17 @@ export type AccountPassword = {
secure_random: Uint8Array,
};
export type storageFileType = 'storage.fileUnknown' | 'storage.filePartial' | 'storage.fileJpeg' |
'storage.fileGif' | 'storage.filePng' | 'storage.filePdf' | 'storage.fileMp3' | 'storage.fileMov' |
'storage.fileMp4' | 'storage.fileWebp';
export type UploadFile = {
_: 'upload.file',
type: storageFileType,
mtime: number,
bytes: Uint8Array
};
export type FileLocation = {
_: 'fileLocationToBeDeprecated',
volume_id: string,

3
webpack.prod.js

@ -73,8 +73,7 @@ module.exports = merge(common, { @@ -73,8 +73,7 @@ module.exports = merge(common, {
|| file.includes('pako')
|| file.includes('Worker.min.js')
|| file.includes('recorder.min.js')
|| file.includes('.hbs')
|| file.includes('mp4box')) return;
|| file.includes('.hbs')) return;
let p = path.resolve(buildDir + file);
if(!newlyCreatedAssets[file] && ['.gz', '.js'].find(ext => file.endsWith(ext)) !== undefined) {

Loading…
Cancel
Save