From a15b0807c4f500601b99adae2b66f9e246eaf5d9 Mon Sep 17 00:00:00 2001 From: morethanwords Date: Tue, 28 Sep 2021 17:07:56 +0400 Subject: [PATCH] Audio fixes Fix loading round video when auto download is off Added unread status for video messages --- src/components/appMediaPlaybackController.ts | 65 ++- src/components/appSearchSuper..ts | 3 +- src/components/audio.ts | 443 ++++++++----------- src/components/popups/newMedia.ts | 4 +- src/components/preloader.ts | 61 ++- src/components/wrappers.ts | 101 +++-- src/helpers/files.ts | 6 +- src/helpers/schedulers.ts | 26 +- src/helpers/schedulers/throttleWith.ts | 22 + src/helpers/schedulers/throttleWithRaf.ts | 9 + src/lib/appManagers/appDocsManager.ts | 4 + src/lib/mediaPlayer.ts | 4 +- src/lib/rootScope.ts | 1 + src/scss/partials/_audio.scss | 2 +- src/scss/style.scss | 17 +- 15 files changed, 384 insertions(+), 384 deletions(-) create mode 100644 src/helpers/schedulers/throttleWith.ts create mode 100644 src/helpers/schedulers/throttleWithRaf.ts diff --git a/src/components/appMediaPlaybackController.ts b/src/components/appMediaPlaybackController.ts index cb94ac38..8c97c546 100644 --- a/src/components/appMediaPlaybackController.ts +++ b/src/components/appMediaPlaybackController.ts @@ -56,6 +56,7 @@ class AppMediaPlaybackController { [mid: string]: CancellablePromise } } = {}; + private waitingDocumentsForLoad: {[docId: string]: Set} = {}; public willBePlayedMedia: HTMLMediaElement; private searchContext: SearchSuperContext; @@ -88,6 +89,15 @@ class AppMediaPlaybackController { } } } + + rootScope.addEventListener('document_downloaded', (doc) => { + const set = this.waitingDocumentsForLoad[doc.id]; + if(set) { + for(const media of set) { + this.onMediaDocumentLoad(media); + } + } + }); } public seekBackward = (details: MediaSessionActionDetails) => { @@ -124,6 +134,7 @@ class AppMediaPlaybackController { //media.muted = true; } + media.dataset.docId = '' + doc.id; media.dataset.peerId = '' + peerId; media.dataset.mid = '' + mid; media.dataset.type = doc.type; @@ -137,6 +148,13 @@ class AppMediaPlaybackController { media.addEventListener('play', this.onPlay); media.addEventListener('pause', this.onPause); media.addEventListener('ended', this.onEnded); + + const message: Message.message = appMessagesManager.getMessageByPeer(peerId, mid); + if(doc.type !== 'audio' && message?.pFlags.media_unread && message.fromId !== rootScope.myId) { + media.addEventListener('timeupdate', () => { + appMessagesManager.readMessages(peerId, [mid]); + }, {once: true}); + } /* const onError = (e: Event) => { //console.log('appMediaPlaybackController: video onError', e); @@ -164,21 +182,44 @@ class AppMediaPlaybackController { //media.autoplay = true; //console.log('will set media url:', media, doc, doc.type, doc.url); - ((!doc.supportsStreaming ? appDocsManager.downloadDoc(doc) : Promise.resolve()) as Promise).then(() => { - if(doc.type === 'audio' && doc.supportsStreaming && SHOULD_USE_SAFARI_FIX) { - this.handleSafariStreamable(media); + const cacheContext = appDownloadManager.getCacheContext(doc); + if(doc.supportsStreaming || cacheContext.url) { + this.onMediaDocumentLoad(media); + } else { + let set = this.waitingDocumentsForLoad[doc.id]; + if(!set) { + set = this.waitingDocumentsForLoad[doc.id] = new Set(); } - - // setTimeout(() => { - const cacheContext = appDownloadManager.getCacheContext(doc); - media.src = cacheContext.url; - // }, doc.supportsStreaming ? 500e3 : 0); - }); + + set.add(media); + appDocsManager.downloadDoc(doc); + } }/* , onError */); return storage[mid] = media; } + private onMediaDocumentLoad = (media: HTMLMediaElement) => { + const doc = appDocsManager.getDoc(media.dataset.docId); + if(doc.type === 'audio' && doc.supportsStreaming && SHOULD_USE_SAFARI_FIX) { + this.handleSafariStreamable(media); + } + + // setTimeout(() => { + const cacheContext = appDownloadManager.getCacheContext(doc); + media.src = cacheContext.url; + // }, doc.supportsStreaming ? 500e3 : 0); + + const set = this.waitingDocumentsForLoad[doc.id]; + if(set) { + set.delete(media); + + if(!set.size) { + delete this.waitingDocumentsForLoad[doc.id]; + } + } + }; + // safari подгрузит последний чанк и песня включится, // при этом этот чанк нельзя руками отдать из SW, потому что браузер тогда теряется private handleSafariStreamable(media: HTMLMediaElement) { @@ -432,10 +473,10 @@ class AppMediaPlaybackController { media.autoplay = true; } */ - this.resolveWaitingForLoadMedia(peerId, mid); - + media.play(); + setTimeout(() => { - media.play()//.catch(() => {}); + this.resolveWaitingForLoadMedia(peerId, mid); }, 0); }; diff --git a/src/components/appSearchSuper..ts b/src/components/appSearchSuper..ts index d9a7ce71..995134d2 100644 --- a/src/components/appSearchSuper..ts +++ b/src/components/appSearchSuper..ts @@ -720,7 +720,8 @@ export default class AppSearchSuper { voiceAsMusic: true, showSender, searchContext: this.copySearchContext(inputFilter), - lazyLoadQueue: this.lazyLoadQueue + lazyLoadQueue: this.lazyLoadQueue, + noAutoDownload: true }); if((['audio', 'voice', 'round'] as MyDocument['type'][]).includes(message.media.document.type)) { diff --git a/src/components/audio.ts b/src/components/audio.ts index ca94e32c..e0cfaaf7 100644 --- a/src/components/audio.ts +++ b/src/components/audio.ts @@ -17,7 +17,7 @@ import rootScope from "../lib/rootScope"; import './middleEllipsis'; import { SearchSuperContext } from "./appSearchSuper."; import { cancelEvent } from "../helpers/dom/cancelEvent"; -import { attachClickEvent, detachClickEvent } from "../helpers/dom/clickEvent"; +import { attachClickEvent } from "../helpers/dom/clickEvent"; import LazyLoadQueue from "./lazyLoadQueue"; import { CancellablePromise, deferredPromise } from "../helpers/cancellablePromise"; import ListenerSetter, { Listener } from "../helpers/listenerSetter"; @@ -28,10 +28,12 @@ import { MiddleEllipsisElement } from "./middleEllipsis"; import htmlToSpan from "../helpers/dom/htmlToSpan"; import { formatFullSentTime } from "../helpers/date"; import { formatBytes } from "../helpers/number"; +import throttleWithRaf from "../helpers/schedulers/throttleWithRaf"; rootScope.addEventListener('messages_media_read', ({mids, peerId}) => { mids.forEach(mid => { - (Array.from(document.querySelectorAll('audio-element[data-mid="' + mid + '"][data-peer-id="' + peerId + '"].is-unread')) as AudioElement[]).forEach(elem => { + const attr = `[data-mid="${mid}"][data-peer-id="${peerId}"]`; + (Array.from(document.querySelectorAll(`audio-element.is-unread${attr}, .media-round.is-unread${attr}`)) as AudioElement[]).forEach(elem => { elem.classList.remove('is-unread'); }); }); @@ -80,11 +82,6 @@ function wrapVoiceMessage(audioEl: AudioElement) { const message = audioEl.message; const doc = (message.media.document || message.media.webpage.document) as MyDocument; - const isOut = message.fromId === rootScope.myId && message.peerId !== rootScope.myId; - let isUnread = message && message.pFlags.media_unread; - if(isUnread) { - audioEl.classList.add('is-unread'); - } if(message.pFlags.out) { audioEl.classList.add('is-out'); @@ -154,113 +151,76 @@ function wrapVoiceMessage(audioEl: AudioElement) { let progress = audioEl.querySelector('.audio-waveform') as HTMLDivElement; const onLoad = () => { - let interval = 0; - let lastIndex = 0; - let audio = audioEl.audio; - if(!audio.paused || (audio.currentTime > 0 && audio.currentTime !== audio.duration)) { - lastIndex = Math.round(audio.currentTime / audio.duration * barCount); - rects.slice(0, lastIndex + 1).forEach(node => node.classList.add('active')); - } - - let start = () => { - clearInterval(interval); - interval = window.setInterval(() => { - if(lastIndex > svg.childElementCount || isNaN(audio.duration) || audio.paused) { - clearInterval(interval); - return; - } - - lastIndex = Math.round(audio.currentTime / audio.duration * barCount); + const onTimeUpdate = () => { + const lastIndex = audio.currentTime === audio.duration ? 0 : Math.ceil(audio.currentTime / audio.duration * barCount); - //svg.children[lastIndex].setAttributeNS(null, 'fill', '#000'); - //svg.children[lastIndex].classList.add('active'); #Иногда пропускает полоски.. - rects.slice(0, lastIndex + 1).forEach(node => node.classList.add('active')); - //++lastIndex; - //console.log('lastIndex:', lastIndex, audio.currentTime); - //}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */); - }, 20); + //svg.children[lastIndex].setAttributeNS(null, 'fill', '#000'); + //svg.children[lastIndex].classList.add('active'); #Иногда пропускает полоски.. + rects.forEach((node, idx) => node.classList.toggle('active', idx < lastIndex)); + //++lastIndex; + //console.log('lastIndex:', lastIndex, audio.currentTime); + //}, duration * 1000 / svg.childElementCount | 0/* 63 * duration / 10 */); }; - if(!audio.paused) { - start(); + if(!audio.paused || (audio.currentTime > 0 && audio.currentTime !== audio.duration)) { + onTimeUpdate(); } - audioEl.addAudioListener('play', () => { - if(isUnread && !isOut && audioEl.classList.contains('is-unread')) { - audioEl.classList.remove('is-unread'); - appMessagesManager.readMessages(audioEl.message.peerId, [audioEl.message.mid]); - isUnread = false; - } + const throttledTimeUpdate = throttleWithRaf(onTimeUpdate); + audioEl.addAudioListener('timeupdate', throttledTimeUpdate); + audioEl.addAudioListener('ended', throttledTimeUpdate); - //rects.forEach(node => node.classList.remove('active')); - start(); - }); - - audioEl.addAudioListener('pause', () => { - clearInterval(interval); - }); - - audioEl.addAudioListener('ended', () => { - clearInterval(interval); - rects.forEach(node => node.classList.remove('active')); - }); - - let mousedown = false, mousemove = false; - progress.addEventListener('mouseleave', (e) => { - if(mousedown) { - audio.play(); - mousedown = false; - } - mousemove = false; - }) - progress.addEventListener('mousemove', (e) => { - mousemove = true; - if(mousedown) scrub(e); - }); - progress.addEventListener('mousedown', (e) => { - e.preventDefault(); - if(e.button !== 1) return; - if(!audio.paused) { - audio.pause(); - } + audioEl.readyPromise.then(() => { + let mousedown = false, mousemove = false; + progress.addEventListener('mouseleave', (e) => { + if(mousedown) { + audio.play(); + mousedown = false; + } + mousemove = false; + }) + progress.addEventListener('mousemove', (e) => { + mousemove = true; + if(mousedown) scrub(e); + }); + progress.addEventListener('mousedown', (e) => { + e.preventDefault(); + if(e.button !== 0) return; + if(!audio.paused) { + audio.pause(); + } + + scrub(e); + mousedown = true; + }); + progress.addEventListener('mouseup', (e) => { + if(mousemove && mousedown) { + audio.play(); + mousedown = false; + } + }); + attachClickEvent(progress, (e) => { + cancelEvent(e); + if(!audio.paused) scrub(e); + }); - scrub(e); - mousedown = true; - }); - progress.addEventListener('mouseup', (e) => { - if (mousemove && mousedown) { - audio.play(); - mousedown = false; + function scrub(e: MouseEvent | TouchEvent) { + let offsetX: number; + if(e instanceof MouseEvent) { + offsetX = e.offsetX; + } else { // touch + const rect = (e.target as HTMLElement).getBoundingClientRect(); + offsetX = e.targetTouches[0].pageX - rect.left; + } + + const scrubTime = offsetX / availW /* width */ * audio.duration; + audio.currentTime = scrubTime; } }); - attachClickEvent(progress, (e) => { - cancelEvent(e); - if(!audio.paused) scrub(e); - }); - - function scrub(e: MouseEvent | TouchEvent) { - let offsetX: number; - if(e instanceof MouseEvent) { - offsetX = e.offsetX; - } else { // touch - const rect = (e.target as HTMLElement).getBoundingClientRect(); - offsetX = e.targetTouches[0].pageX - rect.left; - } - - const scrubTime = offsetX / availW /* width */ * audio.duration; - lastIndex = Math.round(scrubTime / audio.duration * barCount); - - rects.slice(0, lastIndex + 1).forEach(node => node.classList.add('active')); - for(let i = lastIndex + 1; i < rects.length; ++i) { - rects[i].classList.remove('active') - } - audio.currentTime = scrubTime; - } return () => { - clearInterval(interval); progress.remove(); progress = null; audio = null; @@ -367,8 +327,11 @@ function wrapAudio(audioEl: AudioElement) { function constructDownloadPreloader(tryAgainOnFail = true) { const preloader = new ProgressivePreloader({cancelable: true, tryAgainOnFail}); preloader.construct(); - preloader.circle.setAttributeNS(null, 'r', '23'); - preloader.totalLength = 143.58203125; + + if(!tryAgainOnFail) { + preloader.circle.setAttributeNS(null, 'r', '23'); + preloader.totalLength = 143.58203125; + } return preloader; } @@ -388,7 +351,7 @@ export default class AudioElement extends HTMLElement { private listenerSetter = new ListenerSetter(); private onTypeDisconnect: () => void; public onLoad: (autoload?: boolean) => void; - private readyPromise: CancellablePromise; + public readyPromise: CancellablePromise; public render() { this.classList.add('audio'); @@ -414,6 +377,11 @@ export default class AudioElement extends HTMLElement { const downloadDiv = document.createElement('div'); downloadDiv.classList.add('audio-download'); + const isUnread = doc.type !== 'audio' && this.message && this.message.pFlags.media_unread; + if(isUnread) { + this.classList.add('is-unread'); + } + if(uploading) { this.classList.add('is-outgoing'); this.append(downloadDiv); @@ -424,11 +392,17 @@ export default class AudioElement extends HTMLElement { const audioTimeDiv = this.querySelector('.audio-time') as HTMLDivElement; audioTimeDiv.innerHTML = durationStr; - const onLoad = this.onLoad = (autoload = true) => { + const onLoad = this.onLoad = (autoload: boolean) => { this.onLoad = undefined; const audio = this.audio = appMediaPlaybackController.addMedia(this.message.peerId, this.message.media.document || this.message.media.webpage.document, this.message.mid, autoload); + this.readyPromise = deferredPromise(); + if(this.audio.readyState >= 2) this.readyPromise.resolve(); + else { + this.addAudioListener('canplay', () => this.readyPromise.resolve(), {once: true}); + } + this.onTypeDisconnect = onTypeLoad(); const getTimeStr = () => String(audio.currentTime | 0).toHHMMSS() + (isVoice ? (' / ' + durationStr) : ''); @@ -492,174 +466,129 @@ export default class AudioElement extends HTMLElement { if(!isOutgoing) { let preloader: ProgressivePreloader = this.preloader; - const getDownloadPromise = () => appDocsManager.downloadDoc(doc); + onLoad(doc.type !== 'audio' && !this.noAutoDownload); + + if(doc.thumbs) { + const imgs: HTMLImageElement[] = []; + const wrapped = wrapPhoto({ + photo: doc, + message: null, + container: toggle, + boxWidth: 48, + boxHeight: 48, + loadPromises: this.loadPromises, + withoutPreloader: true, + lazyLoadQueue: this.lazyLoadQueue + }); + toggle.style.width = toggle.style.height = ''; + if(wrapped.images.thumb) imgs.push(wrapped.images.thumb); + if(wrapped.images.full) imgs.push(wrapped.images.full); + + this.classList.add('audio-with-thumb'); + imgs.forEach(img => img.classList.add('audio-thumb')); + } - if(isRealVoice) { - if(!preloader) { - preloader = constructDownloadPreloader(); + const r = (shouldPlay: boolean) => { + if(this.audio.src) { + return; } - const load = () => { - const download = getDownloadPromise(); - preloader.attach(downloadDiv, false, download); + appMediaPlaybackController.resolveWaitingForLoadMedia(this.message.peerId, this.message.mid); - if(!downloadDiv.parentElement) { - this.append(downloadDiv); + const onDownloadInit = () => { + if(shouldPlay) { + appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio + + if(IS_SAFARI && !this.audio.autoplay) { + this.audio.autoplay = true; + } } - - (download as Promise).then(() => { - detachClickEvent(this, onClick); - onLoad(); - - downloadDiv.classList.add('downloaded'); - setTimeout(() => { - downloadDiv.remove(); - }, 200); - }); - - return {download}; - }; - - // preloader.construct(); - preloader.setManual(); - preloader.attach(downloadDiv); - preloader.setDownloadFunction(load); - - const onClick = (e?: Event) => { - preloader.onClick(e); }; - - attachClickEvent(this, onClick); - if(!this.noAutoDownload) { - onClick(); - } - } else { - // if(doc.supportsStreaming) { - onLoad(false); - // } - - if(doc.thumbs) { - const imgs: HTMLImageElement[] = []; - const wrapped = wrapPhoto({ - photo: doc, - message: null, - container: toggle, - boxWidth: 48, - boxHeight: 48, - loadPromises: this.loadPromises, - withoutPreloader: true, - lazyLoadQueue: this.lazyLoadQueue - }); - toggle.style.width = toggle.style.height = ''; - if(wrapped.images.thumb) imgs.push(wrapped.images.thumb); - if(wrapped.images.full) imgs.push(wrapped.images.full); - - this.classList.add('audio-with-thumb'); - imgs.forEach(img => img.classList.add('audio-thumb')); - } + onDownloadInit(); - //if(appMediaPlaybackController.mediaExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано - //onLoad(); - //} else { - const r = (e: Event) => { - if(!this.audio) { - const togglePlay = onLoad(false); - } - - if(this.audio.src) { - return; - } - - appMediaPlaybackController.resolveWaitingForLoadMedia(this.message.peerId, this.message.mid); - appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio - - if(IS_SAFARI) { - this.audio.autoplay = true; - } + if(!preloader) { + if(doc.supportsStreaming) { + this.classList.add('corner-download'); + + let pauseListener: Listener; + const onPlay = () => { + const preloader = constructDownloadPreloader(false); + const deferred = deferredPromise(); + deferred.notifyAll({done: 75, total: 100}); + deferred.catch(() => { + this.audio.pause(); + appMediaPlaybackController.willBePlayed(undefined); + }); + deferred.cancel = () => { + deferred.cancel = noop; + const err = new Error(); + (err as any).type = 'CANCELED'; + deferred.reject(err); + }; + preloader.attach(downloadDiv, false, deferred); + + pauseListener = this.addAudioListener('pause', () => { + deferred.cancel(); + }, {once: true}) as any; + + onDownloadInit(); + }; + + /* if(!this.audio.paused) { + onPlay(); + } */ + + const playListener: any = this.addAudioListener('play', onPlay); + this.readyPromise.then(() => { + this.listenerSetter.remove(playListener); + this.listenerSetter.remove(pauseListener); + }); + } else { + preloader = constructDownloadPreloader(); - // togglePlay(undefined, true); + const load = () => { + onDownloadInit(); - this.readyPromise = deferredPromise(); - if(this.audio.readyState >= 2) this.readyPromise.resolve(); - else { - this.addAudioListener('canplay', () => this.readyPromise.resolve(), {once: true}); - } + const download = appDocsManager.downloadDoc(doc); + preloader.attach(downloadDiv, false, download); + return {download}; + }; - if(!preloader) { - if(doc.supportsStreaming) { - this.classList.add('corner-download'); - - let pauseListener: Listener; - const onPlay = () => { - const preloader = constructDownloadPreloader(false); - const deferred = deferredPromise(); - deferred.notifyAll({done: 75, total: 100}); - deferred.catch(() => { - this.audio.pause(); - appMediaPlaybackController.willBePlayed(undefined); - }); - deferred.cancel = () => { - deferred.cancel = noop; - const err = new Error(); - (err as any).type = 'CANCELED'; - deferred.reject(err); - }; - preloader.attach(downloadDiv, false, deferred); - - pauseListener = this.addAudioListener('pause', () => { - deferred.cancel(); - }, {once: true}) as any; - }; - - /* if(!this.audio.paused) { - onPlay(); - } */ - - const playListener: any = this.addAudioListener('play', onPlay); - this.readyPromise.then(() => { - this.listenerSetter.remove(playListener); - this.listenerSetter.remove(pauseListener); - }); - } else { - preloader = constructDownloadPreloader(); - - const load = () => { - const download = getDownloadPromise(); - preloader.attach(downloadDiv, false, download); - return {download}; - }; - - preloader.setDownloadFunction(load); - load(); - } - } + preloader.setDownloadFunction(load); + load(); + } + } - this.append(downloadDiv); + this.append(downloadDiv); - this.classList.add('downloading'); + this.classList.add('downloading'); - this.readyPromise.then(() => { - this.classList.remove('downloading'); - downloadDiv.classList.add('downloaded'); - setTimeout(() => { - downloadDiv.remove(); - }, 200); + this.readyPromise.then(() => { + this.classList.remove('downloading'); + downloadDiv.classList.add('downloaded'); + setTimeout(() => { + downloadDiv.remove(); + }, 200); - //setTimeout(() => { - // release loaded audio - if(appMediaPlaybackController.willBePlayedMedia === this.audio) { - this.audio.play(); - appMediaPlaybackController.willBePlayed(undefined); - } - //}, 10e3); - }); - }; + //setTimeout(() => { + // release loaded audio + if(appMediaPlaybackController.willBePlayedMedia === this.audio) { + this.audio.play(); + appMediaPlaybackController.willBePlayed(undefined); + } + //}, 10e3); + }); + }; - if(!this.audio?.src) { - attachClickEvent(toggle, r, {once: true, capture: true, passive: false}); - } - //} + if(!this.audio?.src) { + if(doc.type !== 'audio' && !this.noAutoDownload) { + r(false); + } else { + attachClickEvent(toggle, () => { + r(true); + }, {once: true, capture: true, passive: false, listenerSetter: this.listenerSetter}); + } } } else if(uploading) { this.preloader.attach(downloadDiv, false); diff --git a/src/components/popups/newMedia.ts b/src/components/popups/newMedia.ts index 88e108b9..31e8f347 100644 --- a/src/components/popups/newMedia.ts +++ b/src/components/popups/newMedia.ts @@ -12,7 +12,7 @@ import { toast } from "../toast"; import { prepareAlbum, wrapDocument } from "../wrappers"; import CheckboxField from "../checkboxField"; import SendContextMenu from "../chat/sendContextMenu"; -import { createPosterFromVideo, onVideoLoad } from "../../helpers/files"; +import { createPosterFromVideo, onMediaLoad } from "../../helpers/files"; import { MyDocument } from "../../lib/appManagers/appDocsManager"; import I18n, { i18n, LangPackKey } from "../../lib/langPack"; import appDownloadManager from "../../lib/appManagers/appDownloadManager"; @@ -262,7 +262,7 @@ export default class PopupNewMedia extends PopupElement { video.pause(); }, {once: true}); - onVideoLoad(video).then(() => { + onMediaLoad(video).then(() => { params.width = video.videoWidth; params.height = video.videoHeight; params.duration = Math.floor(video.duration); diff --git a/src/components/preloader.ts b/src/components/preloader.ts index 8dfc0a0d..7a48b55a 100644 --- a/src/components/preloader.ts +++ b/src/components/preloader.ts @@ -179,7 +179,7 @@ export default class ProgressivePreloader { } } else { if(this.tryAgainOnFail) { - SetTransition(this.preloader, '', true, TRANSITION_TIME); + this.attach(this.preloader.parentElement); fastRaf(() => { this.setManual(); }); @@ -211,47 +211,38 @@ export default class ProgressivePreloader { } public attach(elem: Element, reset = false, promise?: CancellablePromise) { - //return; + if(this.construct) { + this.construct(); + } - this.detached = false; - /* fastRaf(() => { - if(this.detached) return; - this.detached = false; */ + if(this.preloader.parentElement) { + this.preloader.classList.remove('manual'); + } - if(this.construct) { - this.construct(); - } + this.detached = false; - if(this.preloader.parentElement) { - this.preloader.classList.remove('manual'); - } + if(promise/* && false */) { + this.attachPromise(promise); + } + if(this.detached || this.preloader.parentElement !== elem) { const useRafs = isInDOM(this.preloader) ? 1 : 2; if(this.preloader.parentElement !== elem) { elem[this.attachMethod](this.preloader); } - if(promise/* && false */) { - this.attachPromise(promise); - } - - // fastRaf(() => { - //console.log('[PP]: attach after rAF', this.detached, performance.now()); - - // if(this.detached) { - // return; - // } - - SetTransition(this.preloader, 'is-visible', true, TRANSITION_TIME, undefined, useRafs); - // }); + SetTransition(this.preloader, 'is-visible', true, TRANSITION_TIME, undefined, useRafs); + } - if(this.cancelable && reset) { - this.setProgress(0); - } - //}); + if(this.cancelable && reset) { + this.setProgress(0); + } } public detach() { + if(this.detached) { + return; + } //return; this.detached = true; @@ -263,17 +254,17 @@ export default class ProgressivePreloader { /* if(!this.detached) return; this.detached = true; */ - fastRaf(() => { + // fastRaf(() => { //console.log('[PP]: detach after rAF', this.detached, performance.now()); - if(!this.detached || !this.preloader.parentElement) { - return; - } + // if(!this.detached || !this.preloader.parentElement) { + // return; + // } SetTransition(this.preloader, 'is-visible', false, TRANSITION_TIME, () => { this.preloader.remove(); - }); - }); + }, 1); + // }); //})/* , 5e3) */; } } diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index d9a3b327..630a14d0 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -12,7 +12,7 @@ import { formatFullSentTime } from '../helpers/date'; import mediaSizes, { ScreenSize } from '../helpers/mediaSizes'; import { formatBytes } from '../helpers/number'; import { IS_SAFARI } from '../environment/userAgent'; -import { PhotoSize, StickerSet } from '../layer'; +import { Message, PhotoSize, StickerSet } from '../layer'; import appDocsManager, { MyDocument } from "../lib/appManagers/appDocsManager"; import appMessagesManager from '../lib/appManagers/appMessagesManager'; import appPhotosManager, { MyPhoto } from '../lib/appManagers/appPhotosManager'; @@ -31,7 +31,7 @@ import RichTextProcessor from '../lib/richtextprocessor'; import appImManager from '../lib/appManagers/appImManager'; import { SearchSuperContext } from './appSearchSuper.'; import rootScope from '../lib/rootScope'; -import { onVideoLoad } from '../helpers/files'; +import { onMediaLoad } from '../helpers/files'; import { animateSingle } from '../helpers/animation'; import renderImageFromUrl from '../helpers/dom/renderImageFromUrl'; import sequentialDom from '../helpers/sequentialDom'; @@ -77,7 +77,7 @@ mediaSizes.addEventListener('changeScreen', (from, to) => { export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group, onlyPreview, withoutPreloader, loadPromises, noPlayButton, noAutoDownload, size, searchContext}: { doc: MyDocument, container?: HTMLElement, - message?: any, + message?: Message.message, boxWidth?: number, boxHeight?: number, withTail?: boolean, @@ -172,6 +172,8 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai const divRound = document.createElement('div'); divRound.classList.add('media-round', 'z-depth-1'); + divRound.dataset.mid = '' + message.mid; + divRound.dataset.peerId = '' + message.peerId; const size = mediaSizes.active.round; const halfSize = size.width / 2; @@ -190,6 +192,11 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai spanTime.classList.add('tgico'); + const isUnread = message.pFlags.media_unread; + if(isUnread) { + divRound.classList.add('is-unread'); + } + const canvas = document.createElement('canvas'); canvas.width = canvas.height = doc.w/* * window.devicePixelRatio */; @@ -239,6 +246,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai video.classList.add('hide'); divRound.classList.remove('is-paused'); animateSingle(onFrame, canvas); + + if(preloader && preloader.preloader && preloader.preloader.classList.contains('manual')) { + preloader.onClick(); + } }; const onPaused = () => { @@ -352,10 +363,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai const cacheContext = appDownloadManager.getCacheContext(doc); - const isUpload = !!message?.media?.preloader; + const isUpload = !!(message?.media as any)?.preloader; let preloader: ProgressivePreloader; if(isUpload) { // means upload - preloader = message.media.preloader as ProgressivePreloader; + preloader = (message.media as any).preloader as ProgressivePreloader; preloader.attach(container, false); noAutoDownload = undefined; } else if(!cacheContext.downloaded && !doc.supportsStreaming) { @@ -369,6 +380,44 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai }); } + const renderDeferred = deferredPromise(); + video.addEventListener('error', (e) => { + if(video.error.code !== 4) { + console.error("Error " + video.error.code + "; details: " + video.error.message); + } + + if(preloader && !isUpload) { + preloader.detach(); + } + + if(!renderDeferred.isFulfilled) { + renderDeferred.resolve(); + } + }, {once: true}); + + onMediaLoad(video).then(() => { + if(group) { + animationIntersector.addAnimation(video, group); + } + + if(preloader && !isUpload) { + preloader.detach(); + } + + renderDeferred.resolve(); + }); + + if(doc.type === 'video') { + video.addEventListener('timeupdate', () => { + spanTime.innerText = (video.duration - video.currentTime + '').toHHMMSS(false); + }); + } + + video.muted = true; + video.loop = true; + //video.play(); + video.autoplay = true; + let loadPhotoThumbFunc = noAutoDownload && photoRes?.preloader?.loadFunc; const load = () => { if(preloader && noAutoDownload && !withoutPreloader) { @@ -393,20 +442,6 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai } } - video.addEventListener('error', (e) => { - if(video.error.code !== 4) { - console.error("Error " + video.error.code + "; details: " + video.error.message); - } - - if(preloader && !isUpload) { - preloader.detach(); - } - - if(!deferred.isFulfilled) { - deferred.resolve(); - } - }, {once: true}); - if(!noAutoDownload && loadPhotoThumbFunc) { loadPhotoThumbFunc(); loadPhotoThumbFunc = null; @@ -414,10 +449,9 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai noAutoDownload = undefined; - const deferred = deferredPromise(); loadPromise.then(() => { if(middleware && !middleware()) { - deferred.resolve(); + renderDeferred.resolve(); return; } @@ -425,33 +459,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai appMediaPlaybackController.resolveWaitingForLoadMedia(message.peerId, message.mid); } - onVideoLoad(video).then(() => { - if(group) { - animationIntersector.addAnimation(video, group); - } - - if(preloader && !isUpload) { - preloader.detach(); - } - - deferred.resolve(); - }); - - if(doc.type === 'video') { - video.addEventListener('timeupdate', () => { - spanTime.innerText = (video.duration - video.currentTime + '').toHHMMSS(false); - }); - } - - video.muted = true; - video.loop = true; - //video.play(); - video.autoplay = true; - renderImageFromUrl(video, cacheContext.url); }, () => {}); - return {download: loadPromise, render: deferred}; + return {download: loadPromise, render: renderDeferred}; }; if(preloader && !isUpload) { diff --git a/src/helpers/files.ts b/src/helpers/files.ts index 387c4fe6..ef5542ff 100644 --- a/src/helpers/files.ts +++ b/src/helpers/files.ts @@ -69,14 +69,14 @@ export async function createPosterForVideo(url: string) { ]); } -export function onVideoLoad(video: HTMLVideoElement) { +export function onMediaLoad(media: HTMLMediaElement, readyState = media.HAVE_METADATA, useCanplayOnIos?: boolean) { return new Promise((resolve) => { - if(video.readyState >= video.HAVE_METADATA) { + if(media.readyState >= readyState) { resolve(); return; } - video.addEventListener(IS_APPLE_MOBILE ? 'loadeddata' : 'canplay', () => resolve(), {once: true}); + media.addEventListener(IS_APPLE_MOBILE && !useCanplayOnIos ? 'loadeddata' : 'canplay', () => resolve(), {once: true}); }); } diff --git a/src/helpers/schedulers.ts b/src/helpers/schedulers.ts index ec861a0d..8d5e2c9f 100644 --- a/src/helpers/schedulers.ts +++ b/src/helpers/schedulers.ts @@ -7,12 +7,7 @@ // * Jolly Cobra's schedulers import { NoneToVoidFunction } from "../types"; -//type Scheduler = typeof requestAnimationFrame | typeof onTickEnd | typeof runNow; - -/* export function throttleWithRaf(fn: F) { - return throttleWith(fastRaf, fn); -} - +/* export function throttleWithTickEnd(fn: F) { return throttleWith(onTickEnd, fn); } @@ -21,25 +16,6 @@ export function throttleWithNow(fn: F) { return throttleWith(runNow, fn); } -export function throttleWith(schedulerFn: Scheduler, fn: F) { - let waiting = false; - let args: Parameters; - - return (..._args: Parameters) => { - args = _args; - - if (!waiting) { - waiting = true; - - schedulerFn(() => { - waiting = false; - // @ts-ignore - fn(...args); - }); - } - }; -} - export function onTickEnd(cb: NoneToVoidFunction) { Promise.resolve().then(cb); } diff --git a/src/helpers/schedulers/throttleWith.ts b/src/helpers/schedulers/throttleWith.ts new file mode 100644 index 00000000..55518067 --- /dev/null +++ b/src/helpers/schedulers/throttleWith.ts @@ -0,0 +1,22 @@ +// * Jolly Cobra's schedulers + +import { AnyToVoidFunction } from "../../types"; + +export default function throttleWith(schedulerFn: AnyToVoidFunction, fn: F) { + let waiting = false; + let args: Parameters; + + return (..._args: Parameters) => { + args = _args; + + if (!waiting) { + waiting = true; + + schedulerFn(() => { + waiting = false; + // @ts-ignore + fn(...args); + }); + } + }; +} diff --git a/src/helpers/schedulers/throttleWithRaf.ts b/src/helpers/schedulers/throttleWithRaf.ts new file mode 100644 index 00000000..8610bd16 --- /dev/null +++ b/src/helpers/schedulers/throttleWithRaf.ts @@ -0,0 +1,9 @@ +// * Jolly Cobra's schedulers + +import { AnyToVoidFunction } from "../../types"; +import { fastRaf } from "../schedulers"; +import throttleWith from "./throttleWith"; + +export default function throttleWithRaf(fn: F) { + return throttleWith(fastRaf, fn); +} diff --git a/src/lib/appManagers/appDocsManager.ts b/src/lib/appManagers/appDocsManager.ts index 4c90eb9e..0e8ed322 100644 --- a/src/lib/appManagers/appDocsManager.ts +++ b/src/lib/appManagers/appDocsManager.ts @@ -369,6 +369,10 @@ export class AppDocsManager { }); } + download.then(() => { + rootScope.dispatchEvent('document_downloaded', doc); + }); + return download; } diff --git a/src/lib/mediaPlayer.ts b/src/lib/mediaPlayer.ts index 4d12d5c2..230a48a0 100644 --- a/src/lib/mediaPlayer.ts +++ b/src/lib/mediaPlayer.ts @@ -8,7 +8,7 @@ import appMediaPlaybackController from "../components/appMediaPlaybackController import { IS_APPLE_MOBILE } from "../environment/userAgent"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import RangeSelector from "../components/rangeSelector"; -import { onVideoLoad } from "../helpers/files"; +import { onMediaLoad } from "../helpers/files"; import { cancelEvent } from "../helpers/dom/cancelEvent"; import ListenerSetter from "../helpers/listenerSetter"; import ButtonMenu from "../components/buttonMenu"; @@ -427,7 +427,7 @@ export default class VideoPlayer extends EventListenerBase<{ if(video.duration || initDuration) { timeDuration.innerHTML = String(Math.round(video.duration || initDuration)).toHHMMSS(); } else { - onVideoLoad(video).then(() => { + onMediaLoad(video).then(() => { timeDuration.innerHTML = String(Math.round(video.duration)).toHHMMSS(); }); } diff --git a/src/lib/rootScope.ts b/src/lib/rootScope.ts index 1f55aee3..dee5d5d9 100644 --- a/src/lib/rootScope.ts +++ b/src/lib/rootScope.ts @@ -128,6 +128,7 @@ export type BroadcastEvents = { 'download_start': string, 'download_progress': any, + 'document_downloaded': MyDocument, 'context_menu_toggle': boolean }; diff --git a/src/scss/partials/_audio.scss b/src/scss/partials/_audio.scss index 0f853c02..9006abdc 100644 --- a/src/scss/partials/_audio.scss +++ b/src/scss/partials/_audio.scss @@ -402,7 +402,7 @@ } &.active, - .audio.is-unread:not(.is-out) & { + .audio.is-unread:not(.is-out) .audio-toggle:not(.playing) + & { opacity: 1; } } diff --git a/src/scss/style.scss b/src/scss/style.scss index 38112987..67222c14 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -1244,15 +1244,30 @@ middle-ellipsis-element { .video-time { padding: 0 .375rem; background-color: var(--message-highlightning-color) !important; + + &:before, &:after { + margin-left: .25rem; + } + } + + &.is-unread .video-time { + &:before { + order: 1; + width: .5rem; + height: .5rem; + background-color: #fff; + border-radius: 50%; + content: " "; + } } &.is-paused .video-time { &:after { content: $tgico-nosound; - padding-left: .25rem; display: flex; align-items: center; font-size: 1.125rem; + order: 2; } } }