diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index 9203d75b..19e81a82 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -123,6 +123,11 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai }, {once: true}); */ await promise; + } else if(doc.supportsStreaming) { + preloader = new ProgressivePreloader(container, false); + video.addEventListener('canplay', () => { + preloader.detach(); + }, {once: true}); } if(middleware && !middleware()) { diff --git a/src/lib/logger.ts b/src/lib/logger.ts index 53e69076..7be11e03 100644 --- a/src/lib/logger.ts +++ b/src/lib/logger.ts @@ -16,6 +16,8 @@ export function logger(prefix: string, level = LogLevels.log | LogLevels.warn | level = LogLevels.error; } + level = LogLevels.log | LogLevels.warn | LogLevels.error | LogLevels.debug + function Log(...args: any[]) { return level & LogLevels.log && console.log(dT(), '[' + prefix + ']:', ...args); } @@ -37,8 +39,12 @@ export function logger(prefix: string, level = LogLevels.log | LogLevels.warn | }; Log.debug = function(...args: any[]) { - return level & LogLevels.debug && console.debug(dT(), '[' + prefix + ']:', ...args); + return level & LogLevels.debug && console.log(dT(), '[' + prefix + ']:', ...args); }; + + /* Log.debug = function(...args: any[]) { + return level & LogLevels.debug && console.debug(dT(), '[' + prefix + ']:', ...args); + }; */ return Log; }; \ No newline at end of file diff --git a/src/lib/mediaPlayer.ts b/src/lib/mediaPlayer.ts index ec643985..d8864db2 100644 --- a/src/lib/mediaPlayer.ts +++ b/src/lib/mediaPlayer.ts @@ -1,46 +1,132 @@ -export class MediaProgressLine { +export class ProgressLine { public container: HTMLDivElement; - private filled: HTMLDivElement; - private filledLoad: HTMLDivElement; - private seek: HTMLInputElement; + protected filled: HTMLDivElement; + protected seek: HTMLInputElement; - private duration = 0; - private mousedown = false; - private stopAndScrubTimeout = 0; - private progressRAF = 0; + protected duration = 100; + protected mousedown = false; - public onSeek: (time: number) => void; + private events: Partial<{ + //onMouseMove: ProgressLine['onMouseMove'], + onMouseDown: ProgressLine['onMouseDown'], + onMouseUp: ProgressLine['onMouseUp'], + onScrub: (scrubTime: number) => void + }> = {}; - constructor(private media: HTMLAudioElement | HTMLVideoElement, private streamable = false) { + constructor() { this.container = document.createElement('div'); this.container.classList.add('media-progress'); this.filled = document.createElement('div'); this.filled.classList.add('media-progress__filled'); - if(streamable) { - 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'); + const seek = this.seek = document.createElement('input'); seek.classList.add('media-progress__seek'); seek.value = '0'; seek.setAttribute('min', '0'); seek.setAttribute('max', '0'); seek.type = 'range'; seek.step = '0.1'; + seek.max = '' + (this.duration * 1000); - this.setSeekMax(); - this.setListeners(); + //this.setListeners(); + + this.container.append(this.filled, seek); + } + + public setHandlers(events: ProgressLine['events']) { + this.events = events; + } + + onMouseMove = (e: MouseEvent) => { + this.mousedown && this.scrub(e); + }; + + onMouseDown = (e: MouseEvent) => { + this.scrub(e); + this.mousedown = true; + this.events?.onMouseDown(e); + }; + + onMouseUp = (e: MouseEvent) => { + this.mousedown = false; + this.events?.onMouseUp(e); + }; + + protected setListeners() { + this.container.addEventListener('mousemove', this.onMouseMove); + this.container.addEventListener('mousedown', this.onMouseDown); + this.container.addEventListener('mouseup', this.onMouseUp); + } + + protected scrub(e: MouseEvent) { + const scrubTime = e.offsetX / this.container.offsetWidth * this.duration; + + let scaleX = scrubTime / this.duration; + scaleX = Math.max(0, Math.min(1, scaleX)); + this.filled.style.transform = 'scaleX(' + scaleX + ')'; + + //this.events?.onScrub(scrubTime); + return scrubTime; + } + + public removeListeners() { + this.container.removeEventListener('mousemove', this.onMouseMove); + this.container.removeEventListener('mousedown', this.onMouseDown); + this.container.removeEventListener('mouseup', this.onMouseUp); + + this.events = {}; + } +} + +export class MediaProgressLine extends ProgressLine { + private filledLoad: HTMLDivElement; + + private stopAndScrubTimeout = 0; + private progressRAF = 0; + + constructor(private media: HTMLAudioElement | HTMLVideoElement, private streamable = false) { + super(); + + if(streamable) { + this.filledLoad = document.createElement('div'); + this.filledLoad.classList.add('media-progress__filled', 'media-progress__loaded'); + this.container.prepend(this.filledLoad); + //this.setLoadProgress(); + } if(!media.paused || media.currentTime > 0) { this.onPlay(); } - this.container.append(this.filled, seek); + this.setSeekMax(); + this.setListeners(); + this.setHandlers({ + onMouseDown: (e: MouseEvent) => { + //super.onMouseDown(e); + + //Таймер для того, чтобы стопать видео, если зажал мышку и не отпустил клик + if(this.stopAndScrubTimeout) { // возможно лишнее + clearTimeout(this.stopAndScrubTimeout); + } + + this.stopAndScrubTimeout = setTimeout(() => { + !this.media.paused && this.media.pause(); + this.stopAndScrubTimeout = 0; + }, 150); + }, + + onMouseUp: (e: MouseEvent) => { + //super.onMouseUp(e); + + if(this.stopAndScrubTimeout) { + clearTimeout(this.stopAndScrubTimeout); + this.stopAndScrubTimeout = 0; + } + + this.media.paused && this.media.play(); + } + }) } onLoadedData = () => { @@ -70,42 +156,17 @@ export class MediaProgressLine { this.progressRAF = window.requestAnimationFrame(r); }; - onMouseMove = (e: MouseEvent) => { - this.mousedown && this.scrub(e); - }; - - onMouseDown = (e: MouseEvent) => { - this.media.pause(); - this.scrub(e); - - //Таймер для того, чтобы стопать видео, если зажал мышку и не отпустил клик - if(this.stopAndScrubTimeout) { // возможно лишнее - clearTimeout(this.stopAndScrubTimeout); - } - - this.stopAndScrubTimeout = setTimeout(() => { - !this.media.paused && this.media.pause(); - this.stopAndScrubTimeout = 0; - }, 150); - - this.mousedown = true; - }; - - onMouseUp = (e: MouseEvent) => { - if(this.stopAndScrubTimeout) { - clearTimeout(this.stopAndScrubTimeout); - this.stopAndScrubTimeout = 0; - } - - this.media.paused && this.media.play(); - this.mousedown = false; - }; - onProgress = (e: Event) => { this.setLoadProgress(); }; - private setLoadProgress() { + protected scrub(e: MouseEvent) { + const scrubTime = super.scrub(e); + this.media.currentTime = scrubTime; + return scrubTime; + }; + + protected setLoadProgress() { const buf = this.media.buffered; const numRanges = buf.length; @@ -127,7 +188,7 @@ export class MediaProgressLine { this.filledLoad.style.transform = 'scaleX(' + percents + ')'; } - private setSeekMax() { + protected setSeekMax() { this.duration = this.media.duration; if(this.duration > 0) { this.onLoadedData(); @@ -136,7 +197,7 @@ export class MediaProgressLine { } } - private setProgress() { + protected setProgress() { const currentTime = this.media.currentTime; const scaleX = (currentTime / this.duration); @@ -144,41 +205,21 @@ export class MediaProgressLine { this.seek.value = '' + currentTime * 1000; } - private setListeners() { + protected setListeners() { + super.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); - this.container.addEventListener('mouseup', this.onMouseUp); - } - - private scrub(e: MouseEvent) { - const scrubTime = e.offsetX / this.container.offsetWidth * this.duration; - this.media.currentTime = scrubTime; - - if(this.onSeek) { - this.onSeek(scrubTime); - } - - let scaleX = scrubTime / this.duration; - scaleX = Math.max(0, Math.min(1, scaleX)); - this.filled.style.transform = 'scaleX(' + scaleX + ')'; } public removeListeners() { + super.removeListeners(); + 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); - this.container.removeEventListener('mouseup', this.onMouseUp); - - this.onSeek = null; - if(this.stopAndScrubTimeout) { clearTimeout(this.stopAndScrubTimeout); } @@ -217,21 +258,19 @@ export default class VideoPlayer { } private stylePlayer() { - let player = this.wrapper; - let video = this.video; + const {wrapper: player, video, skin} = this; - let skin = this.skin; player.classList.add(skin); - let html = this.buildControls(); + const html = this.buildControls(); player.insertAdjacentHTML('beforeend', html); let updateInterval = 0; let elapsed = 0; let prevTime = 0; if(skin === 'default') { - var toggle = player.querySelectorAll('.toggle') as NodeListOf; - var fullScreenButton = player.querySelector('.fullscreen') as HTMLElement; + const toggle = player.querySelectorAll('.toggle') as NodeListOf; + const fullScreenButton = player.querySelector('.fullscreen') as HTMLElement; var timeElapsed = player.querySelector('#time-elapsed'); var timeDuration = player.querySelector('#time-duration') as HTMLElement; timeDuration.innerHTML = String(video.duration | 0).toHHMMSS(); @@ -263,23 +302,20 @@ export default class VideoPlayer { return this.toggleFullScreen(fullScreenButton); }); - let b = () => this.onFullScreen(); 'webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange'.split(' ').forEach(eventName => { - player.addEventListener(eventName, b, false); + player.addEventListener(eventName, this.onFullScreen, false); }); - } - - if(skin === 'circle') { - let wrapper = document.createElement('div'); + } else if(skin === 'circle') { + const wrapper = document.createElement('div'); wrapper.classList.add('circle-time-left'); video.parentNode.insertBefore(wrapper, video); wrapper.innerHTML = '
'; var circle = player.querySelector('.progress-ring__circle') as SVGCircleElement; - var radius = circle.r.baseVal.value; + const radius = circle.r.baseVal.value; var circumference = 2 * Math.PI * radius; var timeDuration = player.querySelector('.circle-time') as HTMLElement; - var iconVolume = player.querySelector('.iconVolume') as HTMLDivElement; + const iconVolume = player.querySelector('.iconVolume') as HTMLDivElement; circle.style.strokeDasharray = circumference + ' ' + circumference; circle.style.strokeDashoffset = '' + circumference; circle.addEventListener('click', () => { @@ -295,7 +331,7 @@ export default class VideoPlayer { prevTime = video.currentTime; } - let offset = circumference - elapsed / video.duration * circumference; + const offset = circumference - elapsed / video.duration * circumference; circle.style.strokeDashoffset = '' + offset; if(video.paused) clearInterval(updateInterval); }, 20); @@ -339,8 +375,7 @@ export default class VideoPlayer { } private handleProgress(timeDuration: HTMLElement, circumference: number, circle: SVGCircleElement, updateInterval: number) { - let video = this.video; - let skin = this.skin; + const {video, skin} = this; clearInterval(updateInterval); let elapsed = 0; @@ -352,12 +387,13 @@ export default class VideoPlayer { elapsed = video.currentTime; // Update if getCurrentTime was changed prevTime = video.currentTime; } - let offset = circumference - elapsed / video.duration * circumference; + + const offset = circumference - elapsed / video.duration * circumference; circle.style.strokeDashoffset = '' + offset; if(video.paused) clearInterval(updateInterval); }, 20); - let timeLeft = String((video.duration - video.currentTime) | 0).toHHMMSS(); + const timeLeft = String((video.duration - video.currentTime) | 0).toHHMMSS(); if(timeLeft != '0') timeDuration.innerHTML = timeLeft; return updateInterval; @@ -365,22 +401,27 @@ export default class VideoPlayer { } private buildControls() { - let skin = this.skin; - let html = []; + const skin = this.skin; + const html: string[] = []; if(skin === 'default') { - html.push(''); - html.push('
'); - html.push('
'); - html.push('
', - '
', - '
', - '', - ' / ', - '', - '
', - '
', - '
'); - html.push('
'); + html.push(` + +
+
+
+
+ +
+ + / + +
+
+
+ +
+
+
`); } else if(skin === 'circle') { html.push('', '', @@ -391,7 +432,7 @@ export default class VideoPlayer { } public updateButton(toggle: NodeListOf) { - let icon = this.video.paused ? 'tgico-play' : 'tgico-pause'; + const icon = this.video.paused ? 'tgico-play' : 'tgico-pause'; Array.from(toggle).forEach((button) => { button.classList.remove('tgico-play', 'tgico-pause'); button.classList.add(icon); @@ -452,12 +493,12 @@ export default class VideoPlayer { } } - public onFullScreen() { + onFullScreen = () => { // @ts-ignore - let isFullscreenNow = document.webkitFullscreenElement !== null; + const isFullscreenNow = document.webkitFullscreenElement !== null; if(!isFullscreenNow) { this.wrapper.classList.remove('ckin__fullscreen'); } else { } - } + }; } diff --git a/src/lib/mtproto/mtproto.service.ts b/src/lib/mtproto/mtproto.service.ts index 7cc7dd64..c16fced3 100644 --- a/src/lib/mtproto/mtproto.service.ts +++ b/src/lib/mtproto/mtproto.service.ts @@ -17,9 +17,9 @@ const ctx = self as any as ServiceWorkerGlobalScope; //console.error('INCLUDE !!!', new Error().stack); -function isObject(object: any) { +/* function isObject(object: any) { return typeof(object) === 'object' && object !== null; -} +} */ /* function fillTransfer(transfer: any, obj: any) { if(!obj) return; @@ -79,70 +79,72 @@ networkerFactory.setUpdatesProcessor((obj, bool) => { }); const onMessage = async(e: ExtendableMessageEvent) => { - const taskID = e.data.taskID; - - log.debug('got message:', taskID, e, e.data); - - if(e.data.useLs) { - AppStorage.finishTask(e.data.taskID, e.data.args); - return; - } else if(e.data.type == 'convertWebp') { - const {fileName, bytes} = e.data.payload; - const deferred = apiFileManager.webpConvertPromises[fileName]; - if(deferred) { - deferred.resolve(bytes); - delete apiFileManager.webpConvertPromises[fileName]; - } - } - - switch(e.data.task) { - case 'computeSRP': - case 'gzipUncompress': - // @ts-ignore - return cryptoWorker[e.data.task].apply(cryptoWorker, e.data.args).then(result => { - respond(e.source, {taskID: taskID, result: result}); - }); - - case 'cancelDownload': - case 'downloadFile': { - /* // @ts-ignore - return apiFileManager.downloadFile(...e.data.args); */ - - try { - // @ts-ignore - let result = apiFileManager[e.data.task].apply(apiFileManager, e.data.args); - - if(result instanceof Promise) { - result = await result; - } + try { + const taskID = e.data.taskID; - respond(e.source, {taskID: taskID, result: result}); - } catch(err) { - respond(e.source, {taskID: taskID, error: err}); + log.debug('got message:', taskID, e, e.data); + + if(e.data.useLs) { + AppStorage.finishTask(e.data.taskID, e.data.args); + return; + } else if(e.data.type == 'convertWebp') { + const {fileName, bytes} = e.data.payload; + const deferred = apiFileManager.webpConvertPromises[fileName]; + if(deferred) { + deferred.resolve(bytes); + delete apiFileManager.webpConvertPromises[fileName]; } } - - default: { - try { + + switch(e.data.task) { + case 'computeSRP': + case 'gzipUncompress': // @ts-ignore - let result = apiManager[e.data.task].apply(apiManager, e.data.args); - - if(result instanceof Promise) { - result = await result; + return cryptoWorker[e.data.task].apply(cryptoWorker, e.data.args).then(result => { + respond(e.source, {taskID: taskID, result: result}); + }); + + case 'cancelDownload': + case 'downloadFile': { + /* // @ts-ignore + return apiFileManager.downloadFile(...e.data.args); */ + + try { + // @ts-ignore + let result = apiFileManager[e.data.task].apply(apiFileManager, e.data.args); + + if(result instanceof Promise) { + result = await result; + } + + respond(e.source, {taskID: taskID, result: result}); + } catch(err) { + respond(e.source, {taskID: taskID, error: err}); } - - respond(e.source, {taskID: taskID, result: result}); - } catch(err) { - respond(e.source, {taskID: taskID, error: err}); } - - //throw new Error('Unknown task: ' + e.data.task); + + default: { + try { + // @ts-ignore + let result = apiManager[e.data.task].apply(apiManager, e.data.args); + + if(result instanceof Promise) { + result = await result; + } + + respond(e.source, {taskID: taskID, result: result}); + } catch(err) { + respond(e.source, {taskID: taskID, error: err}); + } + + //throw new Error('Unknown task: ' + e.data.task); + } } + } catch(err) { + } }; -ctx.onmessage = onMessage; - /** * Service Worker Installation */ @@ -201,257 +203,274 @@ ctx.onerror = (error) => { log.error('error:', error); }; -const onFetch = (event: FetchEvent): void => { - const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || []; - - log.debug('[fetch]:', event); - - switch(scope) { - case 'download': - case 'thumb': - case 'document': - case 'photo': { - const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); - - const rangeHeader = event.request.headers.get('Range'); - if(rangeHeader && info.mimeType && info.size) { // maybe safari - const range = parseRange(event.request.headers.get('Range')); - const possibleResponse = responseForSafariFirstRange(range, info.mimeType, info.size); - if(possibleResponse) { - return event.respondWith(possibleResponse); - } - } - - const fileName = getFileNameByLocation(info.location, {fileName: info.fileName}); - - /* event.request.signal.addEventListener('abort', (e) => { - console.log('[SW] user aborted request:', fileName); - cancellablePromise.cancel(); - }); - - 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 => { - const responseInit: ResponseInit = {}; - - if(rangeHeader) { - responseInit.headers = { - 'Accept-Ranges': 'bytes', - 'Content-Range': `bytes 0-${info.size - 1}/${info.size || '*'}`, - 'Content-Length': `${info.size}`, - } - } - - return new Response(b, responseInit); - }); - - event.respondWith(Promise.race([ - timeout(45 * 1000), - promise - ])); +ctx.onunhandledrejection = (error) => { + log.error('onunhandledrejection:', error); +}; - break; - } +const onChangeState = () => { + ctx.onmessage = onMessage; + ctx.onfetch = onFetch; +}; - case 'stream': { - const range = parseRange(event.request.headers.get('Range')); - const [offset, end] = range; +onChangeState(); - const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); - //const fileName = getFileNameByLocation(info.location); +ctx.onoffline = ctx.ononline = onChangeState; - log.debug('[stream]', url, offset, end); +const onFetch = (event: FetchEvent): void => { + try { + const [, url, scope, params] = /http[:s]+\/\/.*?(\/(.*?)(?:$|\/(.*)$))/.exec(event.request.url) || []; - event.respondWith(Promise.race([ - timeout(45 * 1000), - new Promise((resolve, reject) => { - // safari workaround + log.debug('[fetch]:', event); + + switch(scope) { + case 'download': + case 'thumb': + case 'document': + case 'photo': { + const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); + + const rangeHeader = event.request.headers.get('Range'); + if(rangeHeader && info.mimeType && info.size) { // maybe safari + const range = parseRange(event.request.headers.get('Range')); const possibleResponse = responseForSafariFirstRange(range, info.mimeType, info.size); if(possibleResponse) { - return resolve(possibleResponse); + return event.respondWith(possibleResponse); } - - 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 = { + } + + const fileName = getFileNameByLocation(info.location, {fileName: info.fileName}); + + /* event.request.signal.addEventListener('abort', (e) => { + console.log('[SW] user aborted request:', fileName); + cancellablePromise.cancel(); + }); + + 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 => { + const responseInit: ResponseInit = {}; + + if(rangeHeader) { + responseInit.headers = { '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) { - ab = ab.slice(offset - alignedOffset, end - alignedOffset + 1); - headers['Content-Range'] = `bytes ${offset}-${offset + ab.byteLength - 1}/${info.size || '*'}`; - headers['Content-Length'] = `${ab.byteLength}`; + 'Content-Range': `bytes 0-${info.size - 1}/${info.size || '*'}`, + 'Content-Length': `${info.size}`, } - - resolve(new Response(ab, { - status: 206, - statusText: 'Partial Content', - headers, - })); - }); - }) - ])); - break; - } - - /* case 'download': { - const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); - - const promise = new Promise((resolve) => { - const headers: Record = { - '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(); + } + + return new Response(b, responseInit); + }); + + event.respondWith(Promise.race([ + timeout(45 * 1000), + promise + ])); + + break; + } + + case 'stream': { + const range = parseRange(event.request.headers.get('Range')); + const [offset, end] = range; + + const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); + //const fileName = getFileNameByLocation(info.location); + + log.debug('[stream]', url, offset, end); + + event.respondWith(Promise.race([ + timeout(45 * 1000), + new Promise((resolve, reject) => { + // safari workaround + const possibleResponse = responseForSafariFirstRange(range, info.mimeType, info.size); + if(possibleResponse) { + return resolve(possibleResponse); + } + + 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 = { + '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) { + ab = ab.slice(offset - alignedOffset, end - alignedOffset + 1); + headers['Content-Range'] = `bytes ${offset}-${offset + ab.byteLength - 1}/${info.size || '*'}`; + headers['Content-Length'] = `${ab.byteLength}`; } - }).catch(err => { - log.error('[download] error:', err); - controller.error(err); + + resolve(new Response(ab, { + status: 206, + statusText: 'Partial Content', + headers, + })); }); - }, - - cancel() { - log.error('[download] file canceled:', info); - } + }) + ])); + break; + } + + /* case 'download': { + const info: DownloadOptions = JSON.parse(decodeURIComponent(params)); + + const promise = new Promise((resolve) => { + const headers: Record = { + '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})); }); - - resolve(new Response(stream, {headers})); - }); - - event.respondWith(promise); - - break; - } */ - - case 'upload': { - if(event.request.method == 'POST') { - event.respondWith(event.request.blob().then(blob => { - return apiFileManager.uploadFile(blob).then(v => new Response(JSON.stringify(v), {headers: {'Content-Type': 'application/json'}})); - })); + + event.respondWith(promise); + + break; + } */ + + case 'upload': { + if(event.request.method == 'POST') { + event.respondWith(event.request.blob().then(blob => { + return apiFileManager.uploadFile(blob).then(v => new Response(JSON.stringify(v), {headers: {'Content-Type': 'application/json'}})); + })); + } + + break; } - - break; - } - - /* default: { - - break; - } - case 'documents': - case 'photos': - case 'profiles': - // direct download - if (event.request.method === 'POST') { - event.respondWith(// download(url, 'unknown file.txt', getFilePartRequest)); - event.request.text() - .then((text) => { - const [, filename] = text.split('='); - return download(url, filename ? filename.toString() : 'unknown file', getFilePartRequest); - }), - ); - - // inline - } else { - event.respondWith( - ctx.cache.match(url).then((cached) => { - if (cached) return cached; - - return Promise.race([ - timeout(45 * 1000), // safari fix - new Promise((resolve) => { - fetchRequest(url, resolve, getFilePartRequest, ctx.cache, fileProgress); + + /* default: { + + break; + } + case 'documents': + case 'photos': + case 'profiles': + // direct download + if (event.request.method === 'POST') { + event.respondWith(// download(url, 'unknown file.txt', getFilePartRequest)); + event.request.text() + .then((text) => { + const [, filename] = text.split('='); + return download(url, filename ? filename.toString() : 'unknown file', getFilePartRequest); }), - ]); - }), - ); + ); + + // inline + } else { + event.respondWith( + ctx.cache.match(url).then((cached) => { + if (cached) return cached; + + return Promise.race([ + timeout(45 * 1000), // safari fix + new Promise((resolve) => { + fetchRequest(url, resolve, getFilePartRequest, ctx.cache, fileProgress); + }), + ]); + }), + ); + } + break; + + case 'stream': { + const [offset, end] = parseRange(event.request.headers.get('Range') || ''); + + log('stream', url, offset, end); + + event.respondWith(new Promise((resolve) => { + fetchStreamRequest(url, offset, end, resolve, getFilePartRequest); + })); + break; } - break; - - case 'stream': { - const [offset, end] = parseRange(event.request.headers.get('Range') || ''); - - log('stream', url, offset, end); - - event.respondWith(new Promise((resolve) => { - fetchStreamRequest(url, offset, end, resolve, getFilePartRequest); - })); - break; - } - - case 'stripped': - case 'cached': { - const bytes = getThumb(url) || null; - event.respondWith(new Response(bytes, { headers: { 'Content-Type': 'image/jpg' } })); - break; + + case 'stripped': + case 'cached': { + const bytes = getThumb(url) || null; + event.respondWith(new Response(bytes, { headers: { 'Content-Type': 'image/jpg' } })); + break; + } + + default: + if (url && url.endsWith('.tgs')) event.respondWith(fetchTGS(url)); + else event.respondWith(fetch(event.request.url)); */ } - - default: - if (url && url.endsWith('.tgs')) event.respondWith(fetchTGS(url)); - else event.respondWith(fetch(event.request.url)); */ + } catch(err) { + event.respondWith(new Response('', { + status: 500, + statusText: 'Internal Server Error', + })); } }; -/** - * Fetch requests - */ -//ctx.addEventListener('fetch', ); -ctx.onfetch = onFetch; - const DOWNLOAD_CHUNK_LIMIT = 512 * 1024; -//const STREAM_CHUNK_UPPER_LIMIT = 256 * 1024; -//const SMALLEST_CHUNK_LIMIT = 256 * 4; -const STREAM_CHUNK_UPPER_LIMIT = 1024 * 1024; -const SMALLEST_CHUNK_LIMIT = 1024 * 4; + +/* const STREAM_CHUNK_UPPER_LIMIT = 256 * 1024; +const SMALLEST_CHUNK_LIMIT = 256 * 4; */ +/* const STREAM_CHUNK_UPPER_LIMIT = 1024 * 1024; +const SMALLEST_CHUNK_LIMIT = 1024 * 4; */ +const STREAM_CHUNK_UPPER_LIMIT = 512 * 1024; +const SMALLEST_CHUNK_LIMIT = 512 * 4; function parseRange(header: string): [number, number] { if(!header) return [0, 0]; diff --git a/src/lib/mtproto/mtprotoworker.ts b/src/lib/mtproto/mtprotoworker.ts index 5682de9f..ed713f04 100644 --- a/src/lib/mtproto/mtprotoworker.ts +++ b/src/lib/mtproto/mtprotoworker.ts @@ -81,10 +81,14 @@ class ApiManagerProxy extends CryptoWorkerMethods { this.finalizeTask(e.data.taskID, e.data.result, e.data.error); } }); + + navigator.serviceWorker.addEventListener('messageerror', (e) => { + this.log.error('SW messageerror:', e); + }); } private finalizeTask(taskID: number, result: any, error: any) { - let deferred = this.awaiting[taskID]; + const deferred = this.awaiting[taskID]; if(deferred !== undefined) { this.log.debug('done', deferred.taskName, result, error); result === undefined ? deferred.reject(error) : deferred.resolve(result); @@ -113,10 +117,12 @@ class ApiManagerProxy extends CryptoWorkerMethods { private releasePending() { if(navigator.serviceWorker.controller) { + this.log.debug('releasing tasks, length:', this.pending.length); this.pending.forEach(pending => { navigator.serviceWorker.controller.postMessage(pending); }); - + + this.log.debug('released tasks'); this.pending.length = 0; } }