Picture-in-Picture
This commit is contained in:
parent
b48bc7610d
commit
28ea417d0a
@ -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();
|
||||
|
@ -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});
|
||||
}
|
||||
|
||||
|
@ -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 => {
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
180
src/components/mediaProgressLine.ts
Normal file
180
src/components/mediaProgressLine.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
84
src/components/volumeSelector.ts
Normal file
84
src/components/volumeSelector.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
@ -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;
|
||||
|
8
src/helpers/dom/createVideo.ts
Normal file
8
src/helpers/dom/createVideo.ts
Normal 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;
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -192,7 +192,7 @@
|
||||
}
|
||||
|
||||
&-day {
|
||||
font-weight: bold;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--primary-text-color) !important;
|
||||
font-size: 14px !important;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-weight: bold;
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin: .75rem 0 .25rem;
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user