Picture-in-Picture

This commit is contained in:
Eduard Kuzmenko 2022-04-15 21:04:56 +03:00
parent 6ba5b94ae7
commit 07fb671e00
19 changed files with 591 additions and 380 deletions

View File

@ -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();

View File

@ -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<AppMediaPlaybackController['setSingleMedia']>;
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<any>).cleanup && (this.listLoader as SearchListLoader<any>).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});
}

View File

@ -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 => {

View File

@ -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;

View File

@ -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);
}

View File

@ -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;
}
}
}

View File

@ -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<void>;
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();

View File

@ -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);
}
};
}

View File

@ -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;

View File

@ -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;
}

View File

@ -173,9 +173,10 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
this.loadedAllUp = true;
}
// if(!this.searchContext.useSearch) {
// this.loadedAllDown = this.loadedAllUp = true;
// }
// it should've been noSearch instead...
if(this.searchContext.useSearch !== false) {
this.loadedAllDown = this.loadedAllUp = true;
}
if(this.otherSideLoader) {
this.otherSideLoader.setSearchContext(context);
@ -270,7 +271,11 @@ export default class SearchListLoader<Item extends {mid: number, peerId: PeerId}
protected setLoaded(down: boolean, value: boolean) {
const changed = super.setLoaded(down, value);
if(changed && this.otherSideLoader && value/* && (this.reverse ? this.loadedAllUp : this.loadedAllDown) */) {
if(changed &&
this.otherSideLoader &&
value &&
this.searchContext?.useSearch !== false/* &&
(this.reverse ? this.loadedAllUp : this.loadedAllDown) */) {
const reverse = this.loadedAllUp;
this.otherSideLoader.setSearchContext({
...this.searchContext,

View File

@ -5,263 +5,19 @@
*/
import appMediaPlaybackController from "../components/appMediaPlaybackController";
import { IS_APPLE_MOBILE } from "../environment/userAgent";
import { IS_APPLE_MOBILE, IS_MOBILE } from "../environment/userAgent";
import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport";
import RangeSelector from "../components/rangeSelector";
import { onMediaLoad } from "../helpers/files";
import cancelEvent from "../helpers/dom/cancelEvent";
import ListenerSetter from "../helpers/listenerSetter";
import ButtonMenu from "../components/buttonMenu";
import { ButtonMenuToggleHandler } from "../components/buttonMenuToggle";
import rootScope from "./rootScope";
import { GrabEvent } from "../helpers/dom/attachGrabListeners";
import { attachClickEvent } from "../helpers/dom/clickEvent";
import ControlsHover from "../helpers/dom/controlsHover";
import { addFullScreenListener, cancelFullScreen, isFullScreen, requestFullScreen } from "../helpers/dom/fullScreen";
import toHHMMSS from "../helpers/string/toHHMMSS";
export 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;
}
}
}
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<HTMLElement>;
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 {
</div>
<div class="right-controls">
<button class="btn-icon ${skin}__button btn-menu-toggle playback-rate night" title="Playback Rate"></button>
${!IS_MOBILE && document.pictureInPictureEnabled ? `<button class="btn-icon ${skin}__button pip tgico-pip" title="Picture-in-Picture"></button>` : ''}
<button class="btn-icon ${skin}__button fullscreen tgico-fullscreen" title="Full Screen"></button>
</div>
</div>
@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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);
}
}

View File

@ -192,7 +192,7 @@
}
&-day {
font-weight: bold;
font-weight: var(--font-weight-bold);
color: var(--primary-text-color) !important;
font-size: 14px !important;
}

View File

@ -17,7 +17,7 @@
}
.chat-title {
font-weight: bold;
font-weight: var(--font-weight-bold);
margin: .75rem 0 .25rem;
line-height: var(--line-height);
}

View File

@ -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);
}
}