diff --git a/src/components/appAudio.ts b/src/components/appAudio.ts deleted file mode 100644 index 27c1316d..00000000 --- a/src/components/appAudio.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { MTDocument } from "../types"; -import { $rootScope } from "../lib/utils"; -import appMessagesManager from "../lib/appManagers/appMessagesManager"; -import appDocsManager from "../lib/appManagers/appDocsManager"; -import opusDecodeController from "../lib/opusDecodeController"; - -// TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда - -class AppAudio { - private container: HTMLElement; - private audios: {[mid: string]: HTMLAudioElement} = {}; - private playingAudio: HTMLAudioElement; - - public willBePlayedAudio: HTMLAudioElement; - - private prevMid: number; - private nextMid: number; - - constructor() { - this.container = document.createElement('div'); - //this.container.style.cssText = 'position: absolute; top: -10000px; left: -10000px;'; - this.container.style.cssText = 'display: none;'; - document.body.append(this.container); - } - - public addAudio(doc: MTDocument, mid: number) { - if(this.audios[mid]) return this.audios[mid]; - - const audio = document.createElement('audio'); - const source = document.createElement('source'); - source.type = doc.type == 'voice' && !opusDecodeController.isPlaySupported() ? 'audio/wav' : doc.mime_type; - - audio.autoplay = false; - audio.volume = 1; - audio.append(source); - - audio.addEventListener('playing', (e) => { - if(this.playingAudio != audio) { - if(this.playingAudio && !this.playingAudio.paused) { - this.playingAudio.pause(); - } - - this.playingAudio = audio; - this.loadSiblingsAudio(doc.type as 'voice' | 'audio', mid); - } - - // audio_pause не успеет сработать без таймаута - setTimeout(() => { - $rootScope.$broadcast('audio_play', {doc, mid}); - }, 0); - }); - - audio.addEventListener('pause', this.onPause); - audio.addEventListener('ended', this.onEnded); - - const onError = (e: Event) => { - if(this.nextMid == mid) { - this.loadSiblingsAudio(doc.type as 'voice' | 'audio', mid).then(() => { - if(this.nextMid && this.audios[this.nextMid]) { - this.audios[this.nextMid].play(); - } - }) - } - }; - - audio.addEventListener('error', onError); - - const downloadPromise: Promise = !doc.supportsStreaming ? appDocsManager.downloadDocNew(doc.id) : Promise.resolve(); - - downloadPromise.then(() => { - this.container.append(audio); - source.src = doc.url; - }, onError); - - return this.audios[mid] = audio; - } - - onPause = (e: Event) => { - $rootScope.$broadcast('audio_pause'); - }; - - onEnded = (e: Event) => { - this.onPause(e); - - if(this.nextMid) { - this.audios[this.nextMid].play(); - } - }; - - private loadSiblingsAudio(type: 'voice' | 'audio', mid: number) { - const audio = this.playingAudio; - const message = appMessagesManager.getMessage(mid); - this.prevMid = this.nextMid = 0; - - return appMessagesManager.getSearch(message.peerID, '', { - _: type == 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterVoice' - }, mid, 3, 0, 2).then(value => { - if(this.playingAudio != audio) { - return; - } - - for(let m of value.history) { - if(m > mid) { - this.nextMid = m; - } else if(m < mid) { - this.prevMid = m; - break; - } - } - - [this.prevMid, this.nextMid].filter(Boolean).forEach(mid => { - const message = appMessagesManager.getMessage(mid); - this.addAudio(message.media.document, mid); - }); - - //console.log('loadSiblingsAudio', audio, type, mid, value, this.prevMid, this.nextMid); - }); - } - - public toggle() { - if(!this.playingAudio) return; - - if(this.playingAudio.paused) { - this.playingAudio.play(); - } else { - this.playingAudio.pause(); - } - } - - public pause() { - if(!this.playingAudio || this.playingAudio.paused) return; - this.playingAudio.pause(); - } - - public willBePlayed(audio: HTMLAudioElement) { - this.willBePlayedAudio = audio; - } - - public audioExists(mid: number) { - return !!this.audios[mid]; - } -} - -const appAudio = new AppAudio(); -// @ts-ignore -if(process.env.NODE_ENV != 'production') { - (window as any).appAudio = appAudio; -} -export default appAudio; \ No newline at end of file diff --git a/src/components/appMediaPlaybackController.ts b/src/components/appMediaPlaybackController.ts new file mode 100644 index 00000000..e7c00d13 --- /dev/null +++ b/src/components/appMediaPlaybackController.ts @@ -0,0 +1,157 @@ +import { MTDocument } from "../types"; +import { $rootScope } from "../lib/utils"; +import appMessagesManager from "../lib/appManagers/appMessagesManager"; +import appDocsManager from "../lib/appManagers/appDocsManager"; + +// TODO: если удалить сообщение, и при этом аудио будет играть - оно не остановится, и можно будет по нему перейти вникуда + +type HTMLMediaElement = HTMLAudioElement | HTMLVideoElement; + +type MediaType = 'voice' | 'audio' | 'round'; + +class AppMediaPlaybackController { + private container: HTMLElement; + private media: {[mid: string]: HTMLMediaElement} = {}; + private playingMedia: HTMLMediaElement; + + public willBePlayedMedia: HTMLMediaElement; + + private prevMid: number; + private nextMid: number; + + constructor() { + this.container = document.createElement('div'); + //this.container.style.cssText = 'position: absolute; top: -10000px; left: -10000px;'; + this.container.style.cssText = 'display: none;'; + document.body.append(this.container); + } + + public addMedia(doc: MTDocument, mid: number): HTMLMediaElement { + if(this.media[mid]) return this.media[mid]; + + const media = document.createElement(doc.type == 'round' ? 'video' : 'audio'); + //const source = document.createElement('source'); + //source.type = doc.type == 'voice' && !opusDecodeController.isPlaySupported() ? 'audio/wav' : doc.mime_type; + + media.autoplay = false; + media.volume = 1; + //media.append(source); + + media.addEventListener('playing', () => { + if(this.playingMedia != media) { + if(this.playingMedia && !this.playingMedia.paused) { + this.playingMedia.pause(); + } + + this.playingMedia = media; + this.loadSiblingsMedia(doc.type as MediaType, mid); + } + + // audio_pause не успеет сработать без таймаута + setTimeout(() => { + $rootScope.$broadcast('audio_play', {doc, mid}); + }, 0); + }); + + media.addEventListener('pause', this.onPause); + media.addEventListener('ended', this.onEnded); + + const onError = (e: Event) => { + if(this.nextMid == mid) { + this.loadSiblingsMedia(doc.type as MediaType, mid).then(() => { + if(this.nextMid && this.media[this.nextMid]) { + this.media[this.nextMid].play(); + } + }); + } + }; + + media.addEventListener('error', onError); + + const downloadPromise: Promise = !doc.supportsStreaming ? appDocsManager.downloadDocNew(doc.id) : Promise.resolve(); + + downloadPromise.then(() => { + //if(doc.type != 'round') { + this.container.append(media); + //} + + //source.src = doc.url; + media.src = doc.url; + }, onError); + + return this.media[mid] = media; + } + + onPause = (e: Event) => { + $rootScope.$broadcast('audio_pause'); + }; + + onEnded = (e: Event) => { + this.onPause(e); + + if(this.nextMid) { + this.media[this.nextMid].play(); + } + }; + + private loadSiblingsMedia(type: MediaType, mid: number) { + const media = this.playingMedia; + const message = appMessagesManager.getMessage(mid); + this.prevMid = this.nextMid = 0; + + return appMessagesManager.getSearch(message.peerID, '', { + //_: type == 'audio' ? 'inputMessagesFilterMusic' : (type == 'round' ? 'inputMessagesFilterRoundVideo' : 'inputMessagesFilterVoice') + _: type == 'audio' ? 'inputMessagesFilterMusic' : 'inputMessagesFilterRoundVoice' + }, mid, 3, 0, 2).then(value => { + if(this.playingMedia != media) { + return; + } + + for(let m of value.history) { + if(m > mid) { + this.nextMid = m; + } else if(m < mid) { + this.prevMid = m; + break; + } + } + + [this.prevMid, this.nextMid].filter(Boolean).forEach(mid => { + const message = appMessagesManager.getMessage(mid); + this.addMedia(message.media.document, mid); + }); + + //console.log('loadSiblingsAudio', audio, type, mid, value, this.prevMid, this.nextMid); + }); + } + + public toggle() { + if(!this.playingMedia) return; + + if(this.playingMedia.paused) { + this.playingMedia.play(); + } else { + this.playingMedia.pause(); + } + } + + public pause() { + if(!this.playingMedia || this.playingMedia.paused) return; + this.playingMedia.pause(); + } + + public willBePlayed(media: HTMLMediaElement) { + this.willBePlayedMedia = media; + } + + public mediaExists(mid: number) { + return !!this.media[mid]; + } +} + +const appMediaPlaybackController = new AppMediaPlaybackController(); +// @ts-ignore +if(process.env.NODE_ENV != 'production') { + (window as any).appMediaPlaybackController = appMediaPlaybackController; +} +export default appMediaPlaybackController; \ No newline at end of file diff --git a/src/components/audio.ts b/src/components/audio.ts index 4260210c..fb567ca4 100644 --- a/src/components/audio.ts +++ b/src/components/audio.ts @@ -3,7 +3,7 @@ import { RichTextProcessor } from "../lib/richtextprocessor"; import { formatDate } from "./wrappers"; import ProgressivePreloader from "./preloader"; import { MediaProgressLine } from "../lib/mediaPlayer"; -import appAudio from "./appAudio"; +import appMediaPlaybackController from "./appMediaPlaybackController"; import { MTDocument } from "../types"; import { mediaSizes } from "../lib/config"; import { Download } from "../lib/appManagers/appDownloadManager"; @@ -313,7 +313,7 @@ export default class AudioElement extends HTMLElement { audioTimeDiv.innerHTML = durationStr; const onLoad = () => { - const audio = this.audio = appAudio.addAudio(doc, mid); + const audio = this.audio = appMediaPlaybackController.addMedia(doc, mid); this.onTypeDisconnect = onTypeLoad(); @@ -390,13 +390,13 @@ export default class AudioElement extends HTMLElement { this.addEventListener('click', onClick); this.click(); } else { - if(appAudio.audioExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано + if(appMediaPlaybackController.mediaExists(mid)) { // чтобы показать прогресс, если аудио уже было скачано onLoad(); } else { const r = () => { onLoad(); - appAudio.willBePlayed(this.audio); // prepare for loading audio + appMediaPlaybackController.willBePlayed(this.audio); // prepare for loading audio if(!preloader) { preloader = new ProgressivePreloader(null, false); @@ -413,9 +413,9 @@ export default class AudioElement extends HTMLElement { //setTimeout(() => { // release loaded audio - if(appAudio.willBePlayedAudio == this.audio) { + if(appMediaPlaybackController.willBePlayedMedia == this.audio) { this.audio.play(); - appAudio.willBePlayedAudio = null; + appMediaPlaybackController.willBePlayedMedia = null; } //}, 10e3); }); diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index ea7d3a83..cba46718 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -1,7 +1,7 @@ import appPhotosManager, { MTPhoto } from '../lib/appManagers/appPhotosManager'; import LottieLoader from '../lib/lottieLoader'; import appDocsManager from "../lib/appManagers/appDocsManager"; -import { formatBytes, getEmojiToneIndex } from "../lib/utils"; +import { formatBytes, getEmojiToneIndex, isInDOM } from "../lib/utils"; import ProgressivePreloader from './preloader'; import LazyLoadQueue from './lazyLoadQueue'; import VideoPlayer from '../lib/mediaPlayer'; @@ -17,6 +17,7 @@ import AudioElement from './audio'; import { DownloadBlob } from '../lib/appManagers/appDownloadManager'; import webpWorkerController from '../lib/webp/webpWorkerController'; import { readBlobAsText } from '../helpers/blob'; +import appMediaPlaybackController from './appMediaPlaybackController'; export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTail, isOut, middleware, lazyLoadQueue, noInfo, group}: { doc: MTDocument, @@ -55,7 +56,72 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai return wrapPhoto(doc, message, container, boxWidth, boxHeight, withTail, isOut, lazyLoadQueue, middleware); } + /* const video = doc.type == 'round' ? appMediaPlaybackController.addMedia(doc, message.mid) as HTMLVideoElement : document.createElement('video'); + if(video.parentElement) { + video.remove(); + } */ + const video = document.createElement('video'); + if(doc.type == 'round') { + video.muted = true; + const globalVideo = appMediaPlaybackController.addMedia(doc, message.mid); + + video.addEventListener('canplay', () => { + if(globalVideo.currentTime > 0) { + video.currentTime = globalVideo.currentTime; + } + + if(!globalVideo.paused) { + // с закоментированными настройками - хром выключал видео при скролле, для этого нужно было включить видео - выйти из диалога, зайти заново и проскроллить вверх + /* video.autoplay = true; + video.loop = false; */ + video.play(); + } + }, {once: true}); + + const clear = () => { + //console.log('clearing video'); + + globalVideo.removeEventListener('timeupdate', onTimeUpdate); + globalVideo.removeEventListener('play', onGlobalPlay); + globalVideo.removeEventListener('pause', onGlobalPause); + video.removeEventListener('play', onVideoPlay); + video.removeEventListener('pause', onVideoPause); + }; + + const onTimeUpdate = () => { + if(!isInDOM(video)) { + clear(); + } + }; + + const onGlobalPlay = () => { + video.play(); + }; + + const onGlobalPause = () => { + video.pause(); + }; + + const onVideoPlay = () => { + globalVideo.play(); + }; + + const onVideoPause = () => { + //console.log('video pause event'); + if(isInDOM(video)) { + globalVideo.pause(); + } else { + clear(); + } + }; + + globalVideo.addEventListener('timeupdate', onTimeUpdate); + globalVideo.addEventListener('play', onGlobalPlay); + globalVideo.addEventListener('pause', onGlobalPause); + video.addEventListener('play', onVideoPlay); + video.addEventListener('pause', onVideoPause); + } let img: HTMLImageElement; if(message) { @@ -154,7 +220,10 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai }, {once: true}); //} - renderImageFromUrl(video, doc.url); + //if(doc.type != 'round') { + renderImageFromUrl(video, doc.url); + //} + video.setAttribute('playsinline', ''); /* if(!container.parentElement) { diff --git a/src/lib/appManagers/appImManager.ts b/src/lib/appManagers/appImManager.ts index f8db0d04..a47819fa 100644 --- a/src/lib/appManagers/appImManager.ts +++ b/src/lib/appManagers/appImManager.ts @@ -35,7 +35,7 @@ import PopupStickers from '../../components/popupStickers'; import SearchInput from '../../components/searchInput'; import AppSearch, { SearchGroup } from '../../components/appSearch'; import PopupDatePicker from '../../components/popupDatepicker'; -import appAudio from '../../components/appAudio'; +import appMediaPlaybackController from '../../components/appMediaPlaybackController'; import appPollsManager from './appPollsManager'; import { ripple } from '../../components/ripple'; import { horizontalMenu } from '../../components/horizontalMenu'; @@ -420,20 +420,20 @@ class ChatAudio { this.container.style.display = 'none'; this.container.parentElement.classList.remove('is-audio-shown'); if(this.toggle.classList.contains('flip-icon')) { - appAudio.toggle(); + appMediaPlaybackController.toggle(); } }); this.toggle.addEventListener('click', (e) => { cancelEvent(e); - appAudio.toggle(); + appMediaPlaybackController.toggle(); }); $rootScope.$on('audio_play', (e: CustomEvent) => { const {doc, mid} = e.detail; let title: string, subtitle: string; - if(doc.type == 'voice') { + if(doc.type == 'voice' || doc.type == 'round') { const message = appMessagesManager.getMessage(mid); title = appPeersManager.getPeerTitle(message.fromID, false, true); //subtitle = 'Voice message'; diff --git a/src/lib/appManagers/appMediaViewer.ts b/src/lib/appManagers/appMediaViewer.ts index cce063ea..d6dc8cb2 100644 --- a/src/lib/appManagers/appMediaViewer.ts +++ b/src/lib/appManagers/appMediaViewer.ts @@ -12,10 +12,9 @@ import AvatarElement from "../../components/avatar"; import LazyLoadQueue from "../../components/lazyLoadQueue"; import appForward from "../../components/appForward"; import { isSafari, mediaSizes, touchSupport } from "../config"; -import appAudio from "../../components/appAudio"; import { deferredPromise } from "../polyfill"; import { MTDocument } from "../../types"; -import idbFileStorage from "../idb"; +import appMediaPlaybackController from "../../components/appMediaPlaybackController"; // TODO: масштабирование картинок (не SVG) при ресайзе, и правильный возврат на исходную позицию // TODO: картинки "обрезаются" если возвращаются или появляются с места, где есть их перекрытие (топбар, поле ввода) @@ -954,8 +953,8 @@ export class AppMediaViewer { video.dataset.overlay = '1'; // fix for simultaneous play - appAudio.pause(); - appAudio.willBePlayedAudio = null; + appMediaPlaybackController.pause(); + appMediaPlaybackController.willBePlayedMedia = null; Promise.all([canPlayThrough, onAnimationEnd]).then(() => { const player = new VideoPlayer(video, true, media.supportsStreaming); diff --git a/src/lib/appManagers/appMessagesManager.ts b/src/lib/appManagers/appMessagesManager.ts index 821095e0..20e64c26 100644 --- a/src/lib/appManagers/appMessagesManager.ts +++ b/src/lib/appManagers/appMessagesManager.ts @@ -2896,7 +2896,7 @@ export class AppMessagesManager { var neededContents: { [type: string]: boolean } = {}, - neededDocType: string | boolean; + neededDocTypes: string[] = []; var neededLimit = limit || 20; var message; @@ -2908,32 +2908,36 @@ export class AppMessagesManager { case 'inputMessagesFilterPhotoVideo': neededContents['messageMediaPhoto'] = true; neededContents['messageMediaDocument'] = true; - neededDocType = 'video'; + neededDocTypes.push('video'); break; case 'inputMessagesFilterVideo': neededContents['messageMediaDocument'] = true; - neededDocType = 'video'; + neededDocTypes.push('video'); break; case 'inputMessagesFilterDocument': neededContents['messageMediaDocument'] = true; - neededDocType = false; break; case 'inputMessagesFilterVoice': neededContents['messageMediaDocument'] = true; - neededDocType = 'voice'; + neededDocTypes.push('voice'); + break; + + case 'inputMessagesFilterRoundVoice': + neededContents['messageMediaDocument'] = true; + neededDocTypes.push('round', 'voice'); break; case 'inputMessagesFilterRoundVideo': neededContents['messageMediaDocument'] = true; - neededDocType = 'round'; + neededDocTypes.push('round'); break; case 'inputMessagesFilterMusic': neededContents['messageMediaDocument'] = true; - neededDocType = 'audio'; + neededDocTypes.push('audio'); break; case 'inputMessagesFilterUrl': @@ -2955,9 +2959,9 @@ export class AppMessagesManager { for(let i = 0; i < historyStorage.history.length; i++) { message = this.messagesStorage[historyStorage.history[i]]; if(message.media && neededContents[message.media._]) { - if(neededDocType !== undefined && + if(neededDocTypes.length && message.media._ == 'messageMediaDocument' && - message.media.document.type != neededDocType) { + !neededDocTypes.includes(message.media.document.type)) { continue; } diff --git a/src/lib/mediaPlayer.ts b/src/lib/mediaPlayer.ts index adcca891..e7b0e6d2 100644 --- a/src/lib/mediaPlayer.ts +++ b/src/lib/mediaPlayer.ts @@ -1,4 +1,4 @@ -import { cancelEvent, whichChild } from "./utils"; +import { cancelEvent } from "./utils"; import { touchSupport } from "./config"; export class ProgressLine { @@ -7,7 +7,7 @@ export class ProgressLine { protected seek: HTMLInputElement; protected duration = 1; - protected mousedown = false; + public mousedown = false; private events: Partial<{ //onMouseMove: ProgressLine['onMouseMove'], @@ -343,7 +343,9 @@ export default class VideoPlayer { volumeSvg.innerHTML = ``; } catch(err) {} - volumeProgress.setProgress(video.muted ? 0 : volume); + if(!volumeProgress.mousedown) { + volumeProgress.setProgress(video.muted ? 0 : volume); + } }; // не вызовется повторно если на 1 установить 1 @@ -439,6 +441,14 @@ export default class VideoPlayer { iconVolume.style.display = ''; }); } + + video.addEventListener('play', () => { + this.wrapper.classList.add('is-playing'); + }); + + video.addEventListener('pause', () => { + this.wrapper.classList.remove('is-playing'); + }); if(video.duration > 0) { timeDuration.innerHTML = String(Math.round(video.duration)).toHHMMSS(); @@ -469,7 +479,7 @@ export default class VideoPlayer { } this.video[this.video.paused ? 'play' : 'pause'](); - this.video.paused ? this.wrapper.classList.remove('is-playing') : this.wrapper.classList.add('is-playing'); + //this.wrapper.classList.toggle('is-playing', !this.video.paused); } private handleProgress(timeDuration: HTMLElement, circumference: number, circle: SVGCircleElement, updateInterval: number) {