diff --git a/src/components/appMediaPlaybackController.ts b/src/components/appMediaPlaybackController.ts index ca720cc4..26ce1e1a 100644 --- a/src/components/appMediaPlaybackController.ts +++ b/src/components/appMediaPlaybackController.ts @@ -23,6 +23,7 @@ import SearchListLoader from "../helpers/searchListLoader"; import { onMediaLoad } from "../helpers/files"; import copy from "../helpers/object/copy"; import deepEqual from "../helpers/object/deepEqual"; +import ListenerSetter from "../helpers/listenerSetter"; // TODO: Safari: проверить стрим, включить его и сразу попробовать включить видео или другую песню // TODO: Safari: попробовать замаскировать подгрузку последнего чанка @@ -92,6 +93,8 @@ export class AppMediaPlaybackController { audio: 1 }; + private pip: HTMLVideoElement; + constructor() { this.container = document.createElement('div'); //this.container.style.cssText = 'position: absolute; top: -10000px; left: -10000px;'; @@ -100,14 +103,14 @@ export class AppMediaPlaybackController { if(navigator.mediaSession) { const actions: {[action in MediaSessionAction]?: MediaSessionActionHandler} = { - play: this.play, - pause: this.pause, - stop: this.stop, - seekbackward: this.seekBackward, - seekforward: this.seekForward, - seekto: this.seekTo, - previoustrack: this.previous, - nexttrack: this.next + play: this.browserPlay, + pause: this.browserPause, + stop: this.browserStop, + seekbackward: this.browserSeekBackward, + seekforward: this.browserSeekForward, + seekto: this.browserSeekTo, + previoustrack: this.browserPrevious, + nexttrack: this.browserNext }; for(const action in actions) { @@ -178,22 +181,19 @@ export class AppMediaPlaybackController { }; } - public seekBackward = (details: MediaSessionActionDetails) => { - const media = this.playingMedia; + public seekBackward = (details: MediaSessionActionDetails, media = this.playingMedia) => { if(media) { media.currentTime = Math.max(0, media.currentTime - (details.seekOffset || SEEK_OFFSET)); } }; - public seekForward = (details: MediaSessionActionDetails) => { - const media = this.playingMedia; + public seekForward = (details: MediaSessionActionDetails, media = this.playingMedia) => { if(media) { media.currentTime = Math.min(media.duration, media.currentTime + (details.seekOffset || SEEK_OFFSET)); } }; - public seekTo = (details: MediaSessionActionDetails) => { - const media = this.playingMedia; + public seekTo = (details: MediaSessionActionDetails, media = this.playingMedia) => { if(media) { media.currentTime = details.seekTime; } @@ -393,6 +393,10 @@ export class AppMediaPlaybackController { } private async setNewMediadata(message: Message.message, playingMedia = this.playingMedia) { + if(document.pictureInPictureElement) { + return; + } + await onMediaLoad(playingMedia, undefined, false); // have to wait for load, otherwise on macOS won't set const doc = appMessagesManager.getMediaFromMessage(message) as MyDocument; @@ -493,6 +497,13 @@ export class AppMediaPlaybackController { navigator.mediaSession.metadata = metadata; } + public setCurrentMediadata() { + const {playingMedia} = this; + if(!playingMedia) return; + const message = this.getMessageByMedia(playingMedia); + this.setNewMediadata(message, playingMedia); + } + private getMessageByMedia(media: HTMLMediaElement): Message.message { const details = this.mediaDetails.get(media); const {peerId, mid} = details; @@ -500,6 +511,21 @@ export class AppMediaPlaybackController { return message; } + public getPlayingDetails() { + const {playingMedia} = this; + if(!playingMedia) { + return; + } + + const message = this.getMessageByMedia(playingMedia); + return { + doc: appMessagesManager.getMediaFromMessage(message), + message, + media: playingMedia, + playbackParams: this.getPlaybackParams() + }; + } + private onPlay = (e?: Event) => { const media = e.target as HTMLMediaElement; const details = this.mediaDetails.get(media); @@ -507,6 +533,11 @@ export class AppMediaPlaybackController { //console.log('appMediaPlaybackController: video playing', this.currentPeerId, this.playingMedia, media); + const pip = this.pip; + if(pip) { + pip.pause(); + } + const message = this.getMessageByMedia(media); const previousMedia = this.playingMedia; @@ -550,21 +581,6 @@ export class AppMediaPlaybackController { }, 0); }; - public getPlayingDetails() { - const {playingMedia} = this; - if(!playingMedia) { - return; - } - - const message = this.getMessageByMedia(playingMedia); - return { - doc: appMessagesManager.getMediaFromMessage(message), - message, - media: playingMedia, - playbackParams: this.getPlaybackParams() - }; - } - private onPause = (e?: Event) => { /* const target = e.target as HTMLMediaElement; if(!isInDOM(target)) { @@ -573,6 +589,10 @@ export class AppMediaPlaybackController { return; } */ + // if(this.pip) { + // this.pip.play(); + // } + rootScope.dispatchEvent('media_pause'); }; @@ -594,23 +614,27 @@ export class AppMediaPlaybackController { } }; - public toggle(play?: boolean) { - if(!this.playingMedia) { + // public get pip() { + // return document.pictureInPictureElement as HTMLVideoElement; + // } + + public toggle(play?: boolean, media = this.playingMedia) { + if(!media) { return false; } if(play === undefined) { - play = this.playingMedia.paused; + play = media.paused; } - if(this.playingMedia.paused !== play) { + if(media.paused !== play) { return false; } if(play) { - this.playingMedia.play(); + media.play(); } else { - this.playingMedia.pause(); + media.pause(); } return true; @@ -624,8 +648,7 @@ export class AppMediaPlaybackController { return this.toggle(false); }; - public stop = () => { - const media = this.playingMedia; + public stop = (media = this.playingMedia) => { if(!media) { return false; } @@ -637,28 +660,30 @@ export class AppMediaPlaybackController { media.currentTime = 0; simulateEvent(media, 'ended'); - const details = this.mediaDetails.get(media); - if(details?.clean) { - media.src = ''; - const peerId = details.peerId; - const s = details.isScheduled ? this.scheduled : this.media; - const storage = s.get(peerId); - if(storage) { - storage.delete(details.mid); - - if(!storage.size) { - s.delete(peerId); + if(media === this.playingMedia) { + const details = this.mediaDetails.get(media); + if(details?.clean) { + media.src = ''; + const peerId = details.peerId; + const s = details.isScheduled ? this.scheduled : this.media; + const storage = s.get(peerId); + if(storage) { + storage.delete(details.mid); + + if(!storage.size) { + s.delete(peerId); + } } + + media.remove(); + + this.mediaDetails.delete(media); } - media.remove(); - - this.mediaDetails.delete(media); + this.playingMedia = undefined; + this.playingMediaType = undefined; } - this.playingMedia = undefined; - this.playingMediaType = undefined; - return true; }; @@ -690,22 +715,45 @@ export class AppMediaPlaybackController { } }; + private bindBrowserCallback(cb: (video: HTMLVideoElement, details: MediaSessionActionDetails) => void) { + const handler: MediaSessionActionHandler = (details) => { + cb(this.pip, details); + }; + + return handler; + } + + public browserPlay = this.bindBrowserCallback((video) => this.toggle(true, video)); + public browserPause = this.bindBrowserCallback((video) => this.toggle(false, video)); + public browserStop = this.bindBrowserCallback((video) => this.stop(video)); + public browserSeekBackward = this.bindBrowserCallback((video, details) => this.seekBackward(details, video)); + public browserSeekForward = this.bindBrowserCallback((video, details) => this.seekForward(details, video)); + public browserSeekTo = this.bindBrowserCallback((video, details) => this.seekTo(details, video)); + public browserNext = this.bindBrowserCallback((video) => video || this.next()); + public browserPrevious = this.bindBrowserCallback((video) => video ? this.seekToStart(video) : this.previous()); + public next = () => { return this.go(1); }; public previous = () => { - const media = this.playingMedia; - // if(media && (media.currentTime > 5 || !this.listLoader.getPrevious().length)) { - if(media && media.currentTime > 5) { - media.currentTime = 0; - this.toggle(true); + if(this.seekToStart(this.playingMedia)) { return; } return this.go(-1); }; + public seekToStart(media: HTMLMediaElement) { + if(media?.currentTime > 5) { + media.currentTime = 0; + this.toggle(true, media); + return true; + } + + return false; + } + public willBePlayed(media: HTMLMediaElement) { this.willBePlayedMedia = media; } @@ -802,7 +850,7 @@ export class AppMediaPlaybackController { else this.playingMedia = undefined; this.toggleSwitchers(false); - return () => { + return (playPaused = wasPlaying) => { this.toggleSwitchers(true); if(playingMedia) { @@ -817,7 +865,7 @@ export class AppMediaPlaybackController { this.stop(); } - if(wasPlaying) { + if(playPaused) { this.play(); } }; @@ -826,6 +874,35 @@ export class AppMediaPlaybackController { public toggleSwitchers(enabled: boolean) { this.lockedSwitchers = !enabled; } + + public setPictureInPicture(video: HTMLVideoElement) { + this.pip = video; + + // let wasPlaying = this.pause(); + + const listenerSetter = new ListenerSetter(); + listenerSetter.add(video)('leavepictureinpicture', () => { + if(this.pip !== video) { + return; + } + + this.pip = undefined; + // if(wasPlaying) { + // this.play(); + // } + + listenerSetter.removeAll(); + }, {once: true}); + + listenerSetter.add(video)('play', () => { + this.pause(); + // if(this.pause()) { + // listenerSetter.add(video)('pause', () => { + // this.play(); + // }, {once: true}); + // } + }); + } } const appMediaPlaybackController = new AppMediaPlaybackController(); diff --git a/src/components/appMediaViewerBase.ts b/src/components/appMediaViewerBase.ts index db3dfb0b..13687ef9 100644 --- a/src/components/appMediaViewerBase.ts +++ b/src/components/appMediaViewerBase.ts @@ -14,7 +14,7 @@ import { logger } from "../lib/logger"; import VideoPlayer from "../lib/mediaPlayer"; import rootScope from "../lib/rootScope"; import animationIntersector from "./animationIntersector"; -import appMediaPlaybackController from "./appMediaPlaybackController"; +import appMediaPlaybackController, { AppMediaPlaybackController } from "./appMediaPlaybackController"; import AvatarElement from "./avatar"; import ButtonIcon from "./buttonIcon"; import { ButtonMenuItemOptions } from "./buttonMenu"; @@ -44,6 +44,8 @@ import RichTextProcessor from "../lib/richtextprocessor"; import { NULL_PEER_ID } from "../lib/mtproto/mtproto_config"; import { isFullScreen } from "../helpers/dom/fullScreen"; import { attachClickEvent } from "../helpers/dom/clickEvent"; +import SearchListLoader from "../helpers/searchListLoader"; +import createVideo from "../helpers/dom/createVideo"; const ZOOM_STEP = 0.5; const ZOOM_INITIAL_VALUE = 1; @@ -114,6 +116,7 @@ export default class AppMediaViewerBase< protected zoomSwipeY = 0; protected ctrlKeyDown: boolean; + protected releaseSingleMedia: ReturnType; get target() { return this.listLoader.current; @@ -426,14 +429,11 @@ export default class AppMediaViewerBase< const promise = this.setMoverToTarget(this.target?.element, true).then(({onAnimationEnd}) => onAnimationEnd); this.listLoader.reset(); - (this.listLoader as any).cleanup && (this.listLoader as any).cleanup(); + (this.listLoader as SearchListLoader).cleanup && (this.listLoader as SearchListLoader).cleanup(); this.setMoverPromise = null; this.tempId = -1; - (window as any).appMediaViewer = undefined; - - if(this.zoomSwipeHandler) { - this.zoomSwipeHandler.removeListeners(); - this.zoomSwipeHandler = undefined; + if((window as any).appMediaViewer === this) { + (window as any).appMediaViewer = undefined; } /* if(appSidebarRight.historyTabIDs.slice(-1)[0] === AppSidebarRight.SLIDERITEMSIDS.forward) { @@ -442,19 +442,48 @@ export default class AppMediaViewerBase< }); } */ - window.removeEventListener('keydown', this.onKeyDown); - window.removeEventListener('keyup', this.onKeyUp); - window.removeEventListener('wheel', this.onWheel, {capture: true}); + this.removeGlobalListeners(); + + this.zoomSwipeHandler = undefined; promise.finally(() => { this.wholeDiv.remove(); - rootScope.isOverlayActive = false; - animationIntersector.checkAnimations(false); + this.toggleOverlay(false); }); return promise; } + protected toggleOverlay(active: boolean) { + rootScope.isOverlayActive = active; + animationIntersector.checkAnimations(active); + } + + protected toggleGlobalListeners(active: boolean) { + if(active) this.setGlobalListeners(); + else this.removeGlobalListeners(); + } + + protected removeGlobalListeners() { + if(this.zoomSwipeHandler) { + this.zoomSwipeHandler.removeListeners(); + } + + window.removeEventListener('keydown', this.onKeyDown); + window.removeEventListener('keyup', this.onKeyUp); + window.removeEventListener('wheel', this.onWheel, {capture: true}); + } + + protected setGlobalListeners() { + if(this.isZooming()) { + this.zoomSwipeHandler.setListeners(); + } + + window.addEventListener('keydown', this.onKeyDown); + window.addEventListener('keyup', this.onKeyUp); + if(!IS_TOUCH_SUPPORTED) window.addEventListener('wheel', this.onWheel, {passive: false, capture: true}); + } + onClick = (e: MouseEvent) => { if(this.setMoverAnimationPromise) return; @@ -770,7 +799,7 @@ export default class AppMediaViewerBase< mediaElement = new Image(); src = target.src; } else if(target instanceof HTMLVideoElement) { - mediaElement = document.createElement('video'); + mediaElement = createVideo(); mediaElement.src = target.src; } else if(target instanceof SVGSVGElement) { const clipId = target.dataset.clipId; @@ -878,10 +907,7 @@ export default class AppMediaViewerBase< mover.classList.add('hiding'); } - this.wholeDiv.classList.add('backwards'); - setTimeout(() => { - this.wholeDiv.classList.remove('active'); - }, 0); + this.toggleWholeActive(false); //return ret; @@ -969,6 +995,17 @@ export default class AppMediaViewerBase< return ret; } + protected toggleWholeActive(active: boolean) { + if(active) { + this.wholeDiv.classList.add('active'); + } else { + this.wholeDiv.classList.add('backwards'); + setTimeout(() => { + this.wholeDiv.classList.remove('active'); + }, 0); + } + } + protected setFullAspect(aspecter: HTMLDivElement, containerRect: DOMRect, rect: DOMRect) { /* let media = aspecter.firstElementChild; let proportion: number; @@ -1209,15 +1246,15 @@ export default class AppMediaViewerBase< this.moveTheMover(this.content.mover, fromRight === 1); this.setNewMover(); } else { - rootScope.isOverlayActive = true; - window.addEventListener('keydown', this.onKeyDown); - window.addEventListener('keyup', this.onKeyUp); - if(!IS_TOUCH_SUPPORTED) window.addEventListener('wheel', this.onWheel, {passive: false, capture: true}); - const mainColumns = document.getElementById('main-columns'); - this.pageEl.insertBefore(this.wholeDiv, mainColumns); - void this.wholeDiv.offsetLeft; // reflow - this.wholeDiv.classList.add('active'); - animationIntersector.checkAnimations(true); + this.toggleOverlay(true); + this.setGlobalListeners(); + + if(!this.wholeDiv.parentElement) { + this.pageEl.insertBefore(this.wholeDiv, document.getElementById('main-columns')); + void this.wholeDiv.offsetLeft; // reflow + } + + this.toggleWholeActive(true); if(!IS_MOBILE_SAFARI) { appNavigationController.pushItem({ @@ -1285,7 +1322,7 @@ export default class AppMediaViewerBase< const useController = message && media.type !== 'gif'; const video = /* useController ? appMediaPlaybackController.addMedia(message, false, true) as HTMLVideoElement : - */document.createElement('video'); + */createVideo({pip: useController}); const set = () => this.setMoverToTarget(target, false, fromRight).then(({onAnimationEnd}) => { //return; // set and don't move @@ -1366,6 +1403,32 @@ export default class AppMediaViewerBase< streamable: supportsStreaming, onPlaybackRackMenuToggle: (open) => { this.wholeDiv.classList.toggle('hide-caption', !!open); + }, + onPip: (pip) => { + if(!pip && (window as any).appMediaViewer !== this) { + this.releaseSingleMedia = undefined; + this.close(); + return; + } + + const mover = this.moversContainer.lastElementChild as HTMLElement; + mover.classList.toggle('hiding', pip); + this.toggleWholeActive(!pip); + this.toggleOverlay(!pip); + this.toggleGlobalListeners(!pip); + + if(useController) { + if(pip) { + // appMediaPlaybackController.toggleSwitchers(true); + + this.releaseSingleMedia(false); + this.releaseSingleMedia = undefined; + + appMediaPlaybackController.setPictureInPicture(video); + } else { + this.releaseSingleMedia = appMediaPlaybackController.setSingleMedia(video, message as Message.message); + } + } } }); player.addEventListener('toggleControls', (show) => { @@ -1374,7 +1437,7 @@ export default class AppMediaViewerBase< this.addEventListener('setMoverBefore', () => { this.wholeDiv.classList.remove('has-video-controls'); - this.videoPlayer.removeListeners(); + this.videoPlayer.cleanup(); this.videoPlayer = undefined; }, {once: true}); @@ -1465,10 +1528,13 @@ export default class AppMediaViewerBase< // * have to set options (especially playbackRate) after src // * https://github.com/videojs/video.js/issues/2516 if(useController) { - const rollback = appMediaPlaybackController.setSingleMedia(video, message as Message.message); + this.releaseSingleMedia = appMediaPlaybackController.setSingleMedia(video, message as Message.message); this.addEventListener('setMoverBefore', () => { - rollback(); + if(this.releaseSingleMedia) { + this.releaseSingleMedia(); + this.releaseSingleMedia = undefined; + } }, {once: true}); } diff --git a/src/components/audio.ts b/src/components/audio.ts index 727b50ff..1c6d39af 100644 --- a/src/components/audio.ts +++ b/src/components/audio.ts @@ -7,7 +7,6 @@ import appDocsManager, {MyDocument} from "../lib/appManagers/appDocsManager"; import { wrapPhoto } from "./wrappers"; import ProgressivePreloader from "./preloader"; -import { MediaProgressLine } from "../lib/mediaPlayer"; import appMediaPlaybackController, { MediaItem, MediaSearchContext } from "./appMediaPlaybackController"; import { DocumentAttribute, Message } from "../layer"; import mediaSizes from "../helpers/mediaSizes"; @@ -32,6 +31,7 @@ import formatBytes from "../helpers/formatBytes"; import { animateSingle } from "../helpers/animation"; import clamp from "../helpers/number/clamp"; import toHHMMSS from "../helpers/string/toHHMMSS"; +import MediaProgressLine from "./mediaProgressLine"; rootScope.addEventListener('messages_media_read', ({mids, peerId}) => { mids.forEach(mid => { diff --git a/src/components/chat/audio.ts b/src/components/chat/audio.ts index 7b0b343d..fd674078 100644 --- a/src/components/chat/audio.ts +++ b/src/components/chat/audio.ts @@ -17,10 +17,11 @@ import replaceContent from "../../helpers/dom/replaceContent"; import PeerTitle from "../peerTitle"; import { i18n } from "../../lib/langPack"; import { formatFullSentTime } from "../../helpers/date"; -import { MediaProgressLine, VolumeSelector } from "../../lib/mediaPlayer"; import ButtonIcon from "../buttonIcon"; import { MyDocument } from "../../lib/appManagers/appDocsManager"; import { Message } from "../../layer"; +import MediaProgressLine from "../mediaProgressLine"; +import VolumeSelector from "../volumeSelector"; export default class ChatAudio extends PinnedContainer { private toggleEl: HTMLElement; diff --git a/src/components/chat/bubbles.ts b/src/components/chat/bubbles.ts index 5e98781f..a8b39055 100644 --- a/src/components/chat/bubbles.ts +++ b/src/components/chat/bubbles.ts @@ -3390,6 +3390,9 @@ export default class ChatBubbles { preview.classList.add('preview'); previewResizer.append(preview); } + + let quoteTextDiv = document.createElement('div'); + quoteTextDiv.classList.add('quote-text'); const doc = webpage.document as MyDocument; if(doc) { @@ -3422,18 +3425,23 @@ export default class ChatBubbles { autoDownloadSize: this.chat.autoDownload.file, lazyLoadQueue: this.lazyLoadQueue, loadPromises, - sizeType: 'documentName' + sizeType: 'documentName', + searchContext: { + useSearch: false, + peerId: this.peerId, + inputFilter: { + _: 'inputMessagesFilterEmpty' + } + } }); preview.append(docDiv); preview.classList.add('preview-with-document'); + quoteTextDiv.classList.add('has-document'); //messageDiv.classList.add((webpage.type || 'document') + '-message'); //doc = null; } } - let quoteTextDiv = document.createElement('div'); - quoteTextDiv.classList.add('quote-text'); - if(previewResizer) { quoteTextDiv.append(previewResizer); } diff --git a/src/components/mediaProgressLine.ts b/src/components/mediaProgressLine.ts new file mode 100644 index 00000000..875a61af --- /dev/null +++ b/src/components/mediaProgressLine.ts @@ -0,0 +1,180 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import { GrabEvent } from "../helpers/dom/attachGrabListeners"; +import appMediaPlaybackController from "./appMediaPlaybackController"; +import RangeSelector from "./rangeSelector"; + +export default class MediaProgressLine extends RangeSelector { + protected filledLoad: HTMLDivElement; + + protected progressRAF = 0; + + protected media: HTMLMediaElement; + protected streamable: boolean; + + constructor(media?: HTMLAudioElement | HTMLVideoElement, streamable?: boolean, withTransition?: boolean, useTransform?: boolean) { + super({ + step: 1000 / 60 / 1000, + min: 0, + max: 1, + withTransition, + useTransform + }, 0); + + if(media) { + this.setMedia(media, streamable); + } + } + + public setMedia(media: HTMLMediaElement, streamable = false) { + if(this.media) { + this.removeListeners(); + } + + if(streamable && !this.filledLoad) { + this.filledLoad = document.createElement('div'); + this.filledLoad.classList.add('progress-line__filled', 'progress-line__loaded'); + this.container.prepend(this.filledLoad); + //this.setLoadProgress(); + } else if(this.filledLoad) { + this.filledLoad.classList.toggle('hide', !streamable); + } + + this.media = media; + this.streamable = streamable; + if(!media.paused || media.currentTime > 0) { + this.onPlay(); + } + + let wasPlaying = false; + this.setSeekMax(); + this.setListeners(); + this.setHandlers({ + onMouseDown: () => { + wasPlaying = !this.media.paused; + wasPlaying && this.media.pause(); + }, + + onMouseUp: (e) => { + // cancelEvent(e.event); + wasPlaying && this.media.play(); + } + }); + } + + protected onLoadedData = () => { + this.max = this.media.duration; + this.seek.setAttribute('max', '' + this.max); + }; + + protected onEnded = () => { + this.setProgress(); + }; + + protected onPlay = () => { + let r = () => { + this.setProgress(); + + this.progressRAF = this.media.paused ? 0 : window.requestAnimationFrame(r); + }; + + if(this.progressRAF) { + window.cancelAnimationFrame(this.progressRAF); + } + + if(this.streamable) { + this.setLoadProgress(); + } + + this.progressRAF = window.requestAnimationFrame(r); + }; + + protected onTimeUpdate = () => { + if(this.media.paused) { + this.setProgress(); + + if(this.streamable) { + this.setLoadProgress(); + } + } + }; + + protected onProgress = (e: Event) => { + this.setLoadProgress(); + }; + + protected scrub(e: GrabEvent) { + const scrubTime = super.scrub(e); + this.media.currentTime = scrubTime; + return scrubTime; + } + + protected setLoadProgress() { + if(appMediaPlaybackController.isSafariBuffering(this.media)) return; + 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.width = (percents * 100) + '%'; + //this.filledLoad.style.transform = 'scaleX(' + percents + ')'; + } + + protected setSeekMax() { + this.max = this.media.duration || 0; + if(this.max > 0) { + this.onLoadedData(); + } else { + this.media.addEventListener('loadeddata', this.onLoadedData); + } + } + + public setProgress() { + if(appMediaPlaybackController.isSafariBuffering(this.media)) return; + const currentTime = this.media.currentTime; + + super.setProgress(currentTime); + } + + public setListeners() { + super.setListeners(); + this.media.addEventListener('ended', this.onEnded); + this.media.addEventListener('play', this.onPlay); + this.media.addEventListener('timeupdate', this.onTimeUpdate); + this.streamable && this.media.addEventListener('progress', this.onProgress); + } + + public removeListeners() { + super.removeListeners(); + + if(this.media) { + this.media.removeEventListener('loadeddata', this.onLoadedData); + this.media.removeEventListener('ended', this.onEnded); + this.media.removeEventListener('play', this.onPlay); + this.media.removeEventListener('timeupdate', this.onTimeUpdate); + this.streamable && this.media.removeEventListener('progress', this.onProgress); + } + + if(this.progressRAF) { + window.cancelAnimationFrame(this.progressRAF); + this.progressRAF = 0; + } + } +} diff --git a/src/components/popups/newMedia.ts b/src/components/popups/newMedia.ts index ef9f3b8c..78ba7611 100644 --- a/src/components/popups/newMedia.ts +++ b/src/components/popups/newMedia.ts @@ -25,6 +25,7 @@ import { attachClickEvent } from "../../helpers/dom/clickEvent"; import MEDIA_MIME_TYPES_SUPPORTED from '../../environment/mediaMimeTypesSupport'; import getGifDuration from "../../helpers/getGifDuration"; import replaceContent from "../../helpers/dom/replaceContent"; +import createVideo from "../../helpers/dom/createVideo"; type SendFileParams = Partial<{ file: File, @@ -275,13 +276,12 @@ export default class PopupNewMedia extends PopupElement { let promise: Promise; if(isVideo) { - const video = document.createElement('video'); + const video = createVideo(); const source = document.createElement('source'); source.src = params.objectURL = URL.createObjectURL(file); video.autoplay = true; video.controls = false; video.muted = true; - video.setAttribute('playsinline', 'true'); video.addEventListener('timeupdate', () => { video.pause(); diff --git a/src/components/volumeSelector.ts b/src/components/volumeSelector.ts new file mode 100644 index 00000000..60658649 --- /dev/null +++ b/src/components/volumeSelector.ts @@ -0,0 +1,84 @@ +/* + * https://github.com/morethanwords/tweb + * Copyright (C) 2019-2021 Eduard Kuzmenko + * https://github.com/morethanwords/tweb/blob/master/LICENSE + */ + +import cancelEvent from "../helpers/dom/cancelEvent"; +import { attachClickEvent } from "../helpers/dom/clickEvent"; +import ListenerSetter from "../helpers/listenerSetter"; +import rootScope from "../lib/rootScope"; +import appMediaPlaybackController from "./appMediaPlaybackController"; +import RangeSelector from "./rangeSelector"; + +export default class VolumeSelector extends RangeSelector { + private static ICONS = ['volume_off', 'volume_mute', 'volume_down', 'volume_up']; + public btn: HTMLElement; + protected icon: HTMLSpanElement; + + constructor(protected listenerSetter: ListenerSetter, protected vertical = false) { + super({ + step: 0.01, + min: 0, + max: 1, + vertical + }, 1); + + this.setListeners(); + this.setHandlers({ + onScrub: currentTime => { + const value = Math.max(Math.min(currentTime, 1), 0); + + //console.log('volume scrub:', currentTime, value); + + appMediaPlaybackController.muted = false; + appMediaPlaybackController.volume = value; + }, + + /* onMouseUp: (e) => { + cancelEvent(e.event); + } */ + }); + + const className = 'player-volume'; + const btn = this.btn = document.createElement('div'); + btn.classList.add('btn-icon', className); + const icon = this.icon = document.createElement('span'); + icon.classList.add(className + '__icon'); + + btn.append(icon, this.container); + + attachClickEvent(icon, this.onMuteClick, {listenerSetter: this.listenerSetter}); + this.listenerSetter.add(rootScope)('media_playback_params', this.setVolume); + + this.setVolume(); + } + + private onMuteClick = (e?: Event) => { + e && cancelEvent(e); + appMediaPlaybackController.muted = !appMediaPlaybackController.muted; + }; + + private setVolume = () => { + // const volume = video.volume; + const {volume, muted} = appMediaPlaybackController; + let d: string; + let iconIndex: number; + if(!volume || muted) { + iconIndex = 0; + } else if(volume > .5) { + iconIndex = 3; + } else if(volume > 0 && volume < .25) { + iconIndex = 1; + } else { + iconIndex = 2; + } + + VolumeSelector.ICONS.forEach(icon => this.icon.classList.remove('tgico-' + icon)); + this.icon.classList.add('tgico-' + VolumeSelector.ICONS[iconIndex]); + + if(!this.mousedown) { + this.setProgress(muted ? 0 : volume); + } + }; +} diff --git a/src/components/wrappers.ts b/src/components/wrappers.ts index f70c3ccc..8bd862ec 100644 --- a/src/components/wrappers.ts +++ b/src/components/wrappers.ts @@ -58,6 +58,7 @@ import Row from './row'; import { ChatAutoDownloadSettings } from '../helpers/autoDownload'; import formatBytes from '../helpers/formatBytes'; import toHHMMSS from '../helpers/string/toHHMMSS'; +import createVideo from '../helpers/dom/createVideo'; const MAX_VIDEO_AUTOPLAY_SIZE = 50 * 1024 * 1024; // 50 MB @@ -183,9 +184,8 @@ export function wrapVideo({doc, container, message, boxWidth, boxHeight, withTai let preloader: ProgressivePreloader; // it must be here, otherwise will get error before initialization in round onPlay - const video = document.createElement('video'); + const video = createVideo(); video.classList.add('media-video'); - video.setAttribute('playsinline', 'true'); video.muted = true; if(doc.type === 'round') { const divRound = document.createElement('div'); @@ -1650,8 +1650,7 @@ export function wrapSticker({doc, div, middleware, lazyLoadQueue, group, play, o if(asStatic) { media = new Image(); } else { - media = document.createElement('video'); - media.setAttribute('playsinline', 'true'); + media = createVideo(); (media as HTMLVideoElement).muted = true; if(play) { @@ -1783,8 +1782,7 @@ export async function wrapStickerSetThumb({set, lazyLoadQueue, container, group, } else { let media: HTMLElement; if(set.pFlags.videos) { - media = document.createElement('video'); - media.setAttribute('playsinline', 'true'); + media = createVideo(); (media as HTMLVideoElement).autoplay = true; (media as HTMLVideoElement).muted = true; (media as HTMLVideoElement).loop = true; diff --git a/src/helpers/dom/createVideo.ts b/src/helpers/dom/createVideo.ts new file mode 100644 index 00000000..031a9893 --- /dev/null +++ b/src/helpers/dom/createVideo.ts @@ -0,0 +1,8 @@ +export default function createVideo(options: { + pip?: boolean +} = {}) { + const video = document.createElement('video'); + if(!options.pip) video.disablePictureInPicture = true; + video.setAttribute('playsinline', 'true'); + return video; +} diff --git a/src/helpers/searchListLoader.ts b/src/helpers/searchListLoader.ts index de0bcb32..2f93fa17 100644 --- a/src/helpers/searchListLoader.ts +++ b/src/helpers/searchListLoader.ts @@ -173,9 +173,10 @@ export default class SearchListLoader 0) { - this.onPlay(); - } - - let wasPlaying = false; - this.setSeekMax(); - this.setListeners(); - this.setHandlers({ - onMouseDown: () => { - wasPlaying = !this.media.paused; - wasPlaying && this.media.pause(); - }, - - onMouseUp: (e) => { - // cancelEvent(e.event); - wasPlaying && this.media.play(); - } - }); - } - - protected onLoadedData = () => { - this.max = this.media.duration; - this.seek.setAttribute('max', '' + this.max); - }; - - protected onEnded = () => { - this.setProgress(); - }; - - protected onPlay = () => { - let r = () => { - this.setProgress(); - - this.progressRAF = this.media.paused ? 0 : window.requestAnimationFrame(r); - }; - - if(this.progressRAF) { - window.cancelAnimationFrame(this.progressRAF); - } - - if(this.streamable) { - this.setLoadProgress(); - } - - this.progressRAF = window.requestAnimationFrame(r); - }; - - protected onTimeUpdate = () => { - if(this.media.paused) { - this.setProgress(); - - if(this.streamable) { - this.setLoadProgress(); - } - } - }; - - protected onProgress = (e: Event) => { - this.setLoadProgress(); - }; - - protected scrub(e: GrabEvent) { - const scrubTime = super.scrub(e); - this.media.currentTime = scrubTime; - return scrubTime; - } - - protected setLoadProgress() { - if(appMediaPlaybackController.isSafariBuffering(this.media)) return; - 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.width = (percents * 100) + '%'; - //this.filledLoad.style.transform = 'scaleX(' + percents + ')'; - } - - protected setSeekMax() { - this.max = this.media.duration || 0; - if(this.max > 0) { - this.onLoadedData(); - } else { - this.media.addEventListener('loadeddata', this.onLoadedData); - } - } - - public setProgress() { - if(appMediaPlaybackController.isSafariBuffering(this.media)) return; - const currentTime = this.media.currentTime; - - super.setProgress(currentTime); - } - - public setListeners() { - super.setListeners(); - this.media.addEventListener('ended', this.onEnded); - this.media.addEventListener('play', this.onPlay); - this.media.addEventListener('timeupdate', this.onTimeUpdate); - this.streamable && this.media.addEventListener('progress', this.onProgress); - } - - public removeListeners() { - super.removeListeners(); - - if(this.media) { - this.media.removeEventListener('loadeddata', this.onLoadedData); - this.media.removeEventListener('ended', this.onEnded); - this.media.removeEventListener('play', this.onPlay); - this.media.removeEventListener('timeupdate', this.onTimeUpdate); - this.streamable && this.media.removeEventListener('progress', this.onProgress); - } - - if(this.progressRAF) { - window.cancelAnimationFrame(this.progressRAF); - this.progressRAF = 0; - } - } -} - -export class VolumeSelector extends RangeSelector { - private static ICONS = ['volume_off', 'volume_mute', 'volume_down', 'volume_up']; - public btn: HTMLElement; - protected icon: HTMLSpanElement; - - constructor(protected listenerSetter: ListenerSetter, protected vertical = false) { - super({ - step: 0.01, - min: 0, - max: 1, - vertical - }, 1); - - this.setListeners(); - this.setHandlers({ - onScrub: currentTime => { - const value = Math.max(Math.min(currentTime, 1), 0); - - //console.log('volume scrub:', currentTime, value); - - appMediaPlaybackController.muted = false; - appMediaPlaybackController.volume = value; - }, - - /* onMouseUp: (e) => { - cancelEvent(e.event); - } */ - }); - - const className = 'player-volume'; - const btn = this.btn = document.createElement('div'); - btn.classList.add('btn-icon', className); - const icon = this.icon = document.createElement('span'); - icon.classList.add(className + '__icon'); - - btn.append(icon, this.container); - - attachClickEvent(icon, this.onMuteClick, {listenerSetter: this.listenerSetter}); - this.listenerSetter.add(rootScope)('media_playback_params', this.setVolume); - - this.setVolume(); - } - - private onMuteClick = (e?: Event) => { - e && cancelEvent(e); - appMediaPlaybackController.muted = !appMediaPlaybackController.muted; - }; - - private setVolume = () => { - // const volume = video.volume; - const {volume, muted} = appMediaPlaybackController; - let d: string; - let iconIndex: number; - if(!volume || muted) { - iconIndex = 0; - } else if(volume > .5) { - iconIndex = 3; - } else if(volume > 0 && volume < .25) { - iconIndex = 1; - } else { - iconIndex = 2; - } - - VolumeSelector.ICONS.forEach(icon => this.icon.classList.remove('tgico-' + icon)); - this.icon.classList.add('tgico-' + VolumeSelector.ICONS[iconIndex]); - - if(!this.mousedown) { - this.setProgress(muted ? 0 : volume); - } - }; -} +import MediaProgressLine from "../components/mediaProgressLine"; +import VolumeSelector from "../components/volumeSelector"; export default class VideoPlayer extends ControlsHover { private static PLAYBACK_RATES = [0.5, 1, 1.5, 2]; @@ -274,18 +30,21 @@ export default class VideoPlayer extends ControlsHover { protected listenerSetter: ListenerSetter; protected playbackRateButton: HTMLElement; + protected pipButton: HTMLElement; /* protected videoParent: HTMLElement; protected videoWhichChild: number; */ protected onPlaybackRackMenuToggle?: (open: boolean) => void; + protected onPip?: (pip: boolean) => void; - constructor({video, play = false, streamable = false, duration, onPlaybackRackMenuToggle}: { + constructor({video, play = false, streamable = false, duration, onPlaybackRackMenuToggle, onPip}: { video: HTMLVideoElement, play?: boolean, streamable?: boolean, duration?: number, - onPlaybackRackMenuToggle?: (open: boolean) => void + onPlaybackRackMenuToggle?: VideoPlayer['onPlaybackRackMenuToggle'], + onPip: VideoPlayer['onPip'] }) { super(); @@ -294,6 +53,7 @@ export default class VideoPlayer extends ControlsHover { this.wrapper.classList.add('ckin__player'); this.onPlaybackRackMenuToggle = onPlaybackRackMenuToggle; + this.onPip = onPip; this.listenerSetter = new ListenerSetter(); @@ -347,6 +107,7 @@ export default class VideoPlayer extends ControlsHover { if(skin === 'default') { this.playbackRateButton = this.wrapper.querySelector('.playback-rate') as HTMLElement; + this.pipButton = this.wrapper.querySelector('.pip') as HTMLElement; const toggle = wrapper.querySelectorAll('.toggle') as NodeListOf; const fullScreenButton = wrapper.querySelector('.fullscreen') as HTMLElement; @@ -366,6 +127,25 @@ export default class VideoPlayer extends ControlsHover { }); }); + if(this.pipButton) { + listenerSetter.add(this.pipButton)('click', () => { + this.video.requestPictureInPicture(); + }); + + const onPip = (pip: boolean) => { + this.wrapper.style.visibility = pip ? 'hidden': ''; + this.onPip(pip); + }; + + listenerSetter.add(video)('enterpictureinpicture', () => { + onPip(true); + }); + + listenerSetter.add(video)('leavepictureinpicture', () => { + onPip(false); + }); + } + if(!IS_TOUCH_SUPPORTED) { listenerSetter.add(video)('click', () => { this.togglePlay(); @@ -406,17 +186,6 @@ export default class VideoPlayer extends ControlsHover { } }); } - - /* player.addEventListener('click', (e) => { - if(e.target !== player) { - return; - } - - this.togglePlay(); - }); */ - - /* video.addEventListener('play', () => { - }); */ listenerSetter.add(video)('dblclick', () => { if(!IS_TOUCH_SUPPORTED) { @@ -492,6 +261,7 @@ export default class VideoPlayer extends ControlsHover {
+ ${!IS_MOBILE && document.pictureInPictureEnabled ? `` : ''}
@@ -593,10 +363,10 @@ export default class VideoPlayer extends ControlsHover { } } - public removeListeners() { + public cleanup() { super.cleanup(); this.listenerSetter.removeAll(); this.progress.removeListeners(); - this.onPlaybackRackMenuToggle = undefined; + this.onPlaybackRackMenuToggle = this.onPip = undefined; } } diff --git a/src/scss/partials/_chat.scss b/src/scss/partials/_chat.scss index 635098ac..0148a663 100644 --- a/src/scss/partials/_chat.scss +++ b/src/scss/partials/_chat.scss @@ -338,7 +338,7 @@ $background-transition-total-time: #{$input-transition-time - $background-transi font-size: 2rem; &:before { - font-weight: bold; + font-weight: var(--font-weight-bold); } } diff --git a/src/scss/partials/_chatBubble.scss b/src/scss/partials/_chatBubble.scss index 91cd6dea..2cc39d05 100644 --- a/src/scss/partials/_chatBubble.scss +++ b/src/scss/partials/_chatBubble.scss @@ -543,6 +543,7 @@ $bubble-beside-button-width: 38px; &.webpage { .preview-with-document { margin-bottom: 0 !important; + min-width: 100%; } .document { @@ -552,6 +553,15 @@ $bubble-beside-button-width: 38px; padding-left: 44px; } } + + .has-document { + display: flex; + flex-direction: column; + + .preview-resizer { + order: 1; + } + } } .preview-resizer { diff --git a/src/scss/partials/_ckin.scss b/src/scss/partials/_ckin.scss index 6cacc832..132b71e4 100644 --- a/src/scss/partials/_ckin.scss +++ b/src/scss/partials/_ckin.scss @@ -87,7 +87,6 @@ padding: 0; position: absolute; opacity: 1; - visibility: visible; top: 50%; left: 50%; transform: translate3d(-50%, -50%, 0) scale(1); @@ -98,7 +97,7 @@ } @include animation-level(2) { - transition: visibility var(--layer-transition), opacity var(--layer-transition); + transition: opacity var(--layer-transition); } @include respond-to(handhelds) { @@ -110,7 +109,6 @@ &:not(.played) { .default__button--big { opacity: 0; - visibility: hidden; } } @@ -205,7 +203,6 @@ &.is-playing, &:not(.played) { .default__button--big { opacity: 0; - visibility: hidden; } .toggle:not(.default__button--big) { diff --git a/src/scss/partials/_poll.scss b/src/scss/partials/_poll.scss index d4977a19..767b1868 100644 --- a/src/scss/partials/_poll.scss +++ b/src/scss/partials/_poll.scss @@ -122,7 +122,7 @@ poll-element { border-radius: 50%; height: 16px; width: 16px; - font-weight: bold; + font-weight: var(--font-weight-bold); font-size: .75rem; opacity: 1; display: flex; @@ -132,7 +132,7 @@ poll-element { &:before { content: $tgico-check; //margin-left: 1px; - font-weight: bold; + font-weight: var(--font-weight-bold); } } diff --git a/src/scss/partials/popups/_datePicker.scss b/src/scss/partials/popups/_datePicker.scss index 2c763194..82ed2722 100644 --- a/src/scss/partials/popups/_datePicker.scss +++ b/src/scss/partials/popups/_datePicker.scss @@ -192,7 +192,7 @@ } &-day { - font-weight: bold; + font-weight: var(--font-weight-bold); color: var(--primary-text-color) !important; font-size: 14px !important; } diff --git a/src/scss/partials/popups/_joinChatInvite.scss b/src/scss/partials/popups/_joinChatInvite.scss index d3744d56..b9fdaa3c 100644 --- a/src/scss/partials/popups/_joinChatInvite.scss +++ b/src/scss/partials/popups/_joinChatInvite.scss @@ -17,7 +17,7 @@ } .chat-title { - font-weight: bold; + font-weight: var(--font-weight-bold); margin: .75rem 0 .25rem; line-height: var(--line-height); } diff --git a/src/scss/tgico.scss b/src/scss/tgico.scss index b21fcfe7..98acb625 100644 --- a/src/scss/tgico.scss +++ b/src/scss/tgico.scss @@ -21,3 +21,10 @@ $tgico-font-path: "assets/fonts" !default; @import "tgico/style"; @import "tgico/variables"; + +.tgico-phone_filled { + &:before { + content: $tgico-endcall_filled; + transform: rotate(-135deg); + } +}