/* * https://github.com/morethanwords/tweb * Copyright (C) 2019-2021 Eduard Kuzmenko * https://github.com/morethanwords/tweb/blob/master/LICENSE */ import appMediaPlaybackController from "../components/appMediaPlaybackController"; import { IS_APPLE_MOBILE } from "../environment/userAgent"; import { IS_TOUCH_SUPPORTED } from "../environment/touchSupport"; import RangeSelector from "../components/rangeSelector"; import { 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 EventListenerBase from "../helpers/eventListenerBase"; import rootScope from "./rootScope"; import findUpClassName from "../helpers/dom/findUpClassName"; import { GrabEvent } from "../helpers/dom/attachGrabListeners"; import { attachClickEvent } from "../helpers/dom/clickEvent"; import getKeyFromEventCaseInsensitive from "../helpers/dom/getKeyFromEventCaseInsensitive"; 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(); 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 { public btn: HTMLElement; protected volumeSvg: HTMLElement; 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); } */ }); this.btn = document.createElement('div'); this.btn.classList.add('player-volume'); this.btn.innerHTML = ` `; this.btn.classList.add('btn-icon'); this.volumeSvg = this.btn.firstElementChild as HTMLElement; this.btn.append(this.container); attachClickEvent(this.volumeSvg, 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; if(!volume || muted) { d = `M16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zm2.5 0c0 .94-.2 1.82-.54 2.64l1.51 1.51C20.63 14.91 21 13.5 21 12c0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM4.27 3L3 4.27 7.73 9H3v6h4l5 5v-6.73l4.25 4.25c-.67.52-1.42.93-2.25 1.18v2.06c1.38-.31 2.63-.95 3.69-1.81L19.73 21 21 19.73l-9-9L4.27 3zM12 4L9.91 6.09 12 8.18V4z`; } else if(volume > .5) { d = `M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z`; } else if(volume > 0 && volume < .25) { d = `M7 9v6h4l5 5V4l-5 5H7z`; } else { d = `M18.5 12c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z`; } try { this.volumeSvg.innerHTML = ``; } catch(err) {} if(!this.mousedown) { this.setProgress(muted ? 0 : volume); } }; } export default class VideoPlayer extends EventListenerBase<{ toggleControls: (show: boolean) => void }> { private wrapper: HTMLDivElement; private progress: MediaProgressLine; private skin: 'default'; private listenerSetter: ListenerSetter; private showControlsTimeout = 0; private controlsLocked: boolean; /* private videoParent: HTMLElement; private videoWhichChild: number; */ constructor(private video: HTMLVideoElement, play = false, streamable = false, duration?: number) { super(false); this.wrapper = document.createElement('div'); this.wrapper.classList.add('ckin__player'); this.listenerSetter = new ListenerSetter(); video.parentNode.insertBefore(this.wrapper, video); this.wrapper.appendChild(video); this.skin = 'default'; this.stylePlayer(duration); // this.setBtnMenuToggle(); if(this.skin === 'default') { const controls = this.wrapper.querySelector('.default__controls.ckin__controls') as HTMLDivElement; this.progress = new MediaProgressLine(video, streamable); controls.prepend(this.progress.container); } if(play/* && video.paused */) { const promise = video.play(); promise.catch((err: Error) => { if(err.name === 'NotAllowedError') { video.muted = true; video.autoplay = true; video.play(); } }).finally(() => { // due to autoplay, play will not call this.wrapper.classList.toggle('is-playing', !this.video.paused); }); //(this.wrapper.querySelector('.toggle') as HTMLButtonElement).click(); } } private stylePlayer(initDuration: number) { const {wrapper: player, video, skin} = this; player.classList.add(skin); const html = this.buildControls(); player.insertAdjacentHTML('beforeend', html); let timeDuration: HTMLElement; if(skin === 'default') { const toggle = player.querySelectorAll('.toggle') as NodeListOf; const fullScreenButton = player.querySelector('.fullscreen') as HTMLElement; const timeElapsed = player.querySelector('#time-elapsed'); timeDuration = player.querySelector('#time-duration') as HTMLElement; timeDuration.innerHTML = String(video.duration | 0).toHHMMSS(); const volumeSelector = new VolumeSelector(this.listenerSetter); const leftControls = player.querySelector('.left-controls'); volumeSelector.btn.classList.remove('btn-icon'); leftControls.insertBefore(volumeSelector.btn, timeElapsed.parentElement); Array.from(toggle).forEach((button) => { this.listenerSetter.add(button)('click', () => { this.togglePlay(); }); }); this.listenerSetter.add(video)('click', () => { if(!IS_TOUCH_SUPPORTED) { this.togglePlay(); } }); if(IS_TOUCH_SUPPORTED) { this.listenerSetter.add(player)('click', () => { this.toggleControls(); }); /* this.listenerSetter.add(player)('touchstart', () => { showControls(false); }); this.listenerSetter.add(player)('touchend', () => { if(player.classList.contains('is-playing')) { showControls(); } }); */ } else { this.listenerSetter.add(this.wrapper)('mousemove', () => { this.showControls(); }); this.listenerSetter.add(this.wrapper)('mouseenter', () => { this.showControls(false); }); this.listenerSetter.add(this.wrapper)('mouseleave', (e) => { if(findUpClassName(e.relatedTarget, 'media-viewer-caption')) { this.showControls(false); return; } this.hideControls(); }); this.listenerSetter.add(document)('keydown', (e: KeyboardEvent) => { if(rootScope.overlaysActive > 1) { // forward popup is active, etc return; } const key = getKeyFromEventCaseInsensitive(e); let good = true; if(key === 'F') { this.toggleFullScreen(fullScreenButton); } else if(key === 'M') { appMediaPlaybackController.muted = !appMediaPlaybackController.muted; } else if(key === ' ') { this.togglePlay(); } else if(e.altKey && key === '=') { appMediaPlaybackController.playbackRate += .25; } else if(e.altKey && key === '-') { appMediaPlaybackController.playbackRate -= .25; } else if(this.wrapper.classList.contains('ckin__fullscreen') && (key === 'ArrowLeft' || key === 'ArrowRight')) { if(key === 'ArrowLeft') appMediaPlaybackController.seekBackward({action: 'seekbackward'}); else appMediaPlaybackController.seekForward({action: 'seekforward'}); } else { good = false; } if(good) { cancelEvent(e); return false; } }); } /* player.addEventListener('click', (e) => { if(e.target !== player) { return; } this.togglePlay(); }); */ /* video.addEventListener('play', () => { }); */ this.listenerSetter.add(video)('dblclick', () => { if(!IS_TOUCH_SUPPORTED) { this.toggleFullScreen(fullScreenButton); } }); this.listenerSetter.add(fullScreenButton)('click', (e) => { this.toggleFullScreen(fullScreenButton); }); 'webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange'.split(' ').forEach(eventName => { this.listenerSetter.add(player)(eventName, this.onFullScreen, false); }); this.listenerSetter.add(video)('timeupdate', () => { timeElapsed.innerHTML = String(video.currentTime | 0).toHHMMSS(); }); this.listenerSetter.add(video)('play', () => { this.wrapper.classList.add('played'); }, {once: true}); this.listenerSetter.add(video)('pause', () => { this.showControls(false); }); } this.listenerSetter.add(video)('play', () => { this.wrapper.classList.add('is-playing'); }); this.listenerSetter.add(video)('pause', () => { this.wrapper.classList.remove('is-playing'); }); if(video.duration || initDuration) { timeDuration.innerHTML = String(Math.round(video.duration || initDuration)).toHHMMSS(); } else { onMediaLoad(video).then(() => { timeDuration.innerHTML = String(Math.round(video.duration)).toHHMMSS(); }); } } public hideControls = () => { clearTimeout(this.showControlsTimeout); this.showControlsTimeout = 0; const isShown = this.wrapper.classList.contains('show-controls'); if(this.controlsLocked !== false) { if(this.video.paused || !isShown || this.controlsLocked) { return; } } else if(!isShown) { return; } this.dispatchEvent('toggleControls', false); this.wrapper.classList.remove('show-controls'); }; public showControls = (setHideTimeout = true) => { if(this.showControlsTimeout) { clearTimeout(this.showControlsTimeout); this.showControlsTimeout = 0; } else if(!this.wrapper.classList.contains('show-controls') && this.controlsLocked !== false) { this.dispatchEvent('toggleControls', true); this.wrapper.classList.add('show-controls'); } if(!setHideTimeout || this.controlsLocked) { return; } this.showControlsTimeout = window.setTimeout(this.hideControls, 3e3); }; public toggleControls = (show?: boolean) => { const isShown = this.wrapper.classList.contains('show-controls'); if(show === undefined) { if(isShown) this.hideControls(); else this.showControls(); } else if(show === isShown) return; else if(show === false) this.hideControls(); else this.showControls(); }; public lockControls(visible: boolean) { this.controlsLocked = visible; this.wrapper.classList.toggle('disable-hover', visible === false); this.toggleControls(visible); } protected togglePlay() { this.video[this.video.paused ? 'play' : 'pause'](); } private buildControls() { const skin = this.skin; if(skin === 'default') { return `
/
`; } } protected setBtnMenuToggle() { const buttons: Parameters[0] = [0.25, 0.5, 1, 1.25, 1.5, 2].map((rate) => { return { regularText: rate === 1 ? 'Normal' : '' + rate, onClick: () => this.video.playbackRate = rate }; }); const btnMenu = ButtonMenu(buttons); const settingsButton = this.wrapper.querySelector('.settings') as HTMLElement; btnMenu.classList.add('top-left'); ButtonMenuToggleHandler(settingsButton); settingsButton.append(btnMenu); } public static isFullScreen(): boolean { // @ts-ignore return !!(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement); } protected toggleFullScreen(fullScreenButton: HTMLElement) { // alternative standard method const player = this.wrapper; // * https://caniuse.com/#feat=fullscreen if(IS_APPLE_MOBILE) { const video = this.video as any; video.webkitEnterFullscreen(); video.enterFullscreen(); return; } if(!VideoPlayer.isFullScreen()) { player.classList.add('ckin__fullscreen'); /* const videoParent = this.video.parentElement; const videoWhichChild = whichChild(this.video); const needVideoRemount = videoParent !== player; if(needVideoRemount) { this.videoParent = videoParent; this.videoWhichChild = videoWhichChild; player.prepend(this.video); } */ if(player.requestFullscreen) { player.requestFullscreen(); // @ts-ignore } else if(player.mozRequestFullScreen) { // @ts-ignore player.mozRequestFullScreen(); // Firefox // @ts-ignore } else if(player.webkitRequestFullscreen) { // @ts-ignore player.webkitRequestFullscreen(); // Chrome and Safari // @ts-ignore } else if(player.msRequestFullscreen) { // @ts-ignore player.msRequestFullscreen(); } fullScreenButton.classList.remove('tgico-fullscreen'); fullScreenButton.classList.add('tgico-smallscreen'); fullScreenButton.setAttribute('title', 'Exit Full Screen'); } else { player.classList.remove('ckin__fullscreen'); /* if(this.videoParent) { const {videoWhichChild, videoParent} = this; if(!videoWhichChild) { videoParent.prepend(this.video); } else { videoParent.insertBefore(this.video, videoParent.children[videoWhichChild]); } this.videoParent = null; this.videoWhichChild = -1; } */ // @ts-ignore if(document.cancelFullScreen) { // @ts-ignore document.cancelFullScreen(); // @ts-ignore } else if(document.mozCancelFullScreen) { // @ts-ignore document.mozCancelFullScreen(); // @ts-ignore } else if(document.webkitCancelFullScreen) { // @ts-ignore document.webkitCancelFullScreen(); // @ts-ignore } else if(document.msExitFullscreen) { // @ts-ignore document.msExitFullscreen(); } fullScreenButton.classList.remove('tgico-smallscreen'); fullScreenButton.classList.add('tgico-fullscreen'); fullScreenButton.setAttribute('title', 'Full Screen'); } } protected onFullScreen = () => { // @ts-ignore const isFullscreenNow = document.webkitFullscreenElement !== null; if(!isFullscreenNow) { this.wrapper.classList.remove('ckin__fullscreen'); } }; public removeListeners() { super.cleanup(); this.listenerSetter.removeAll(); this.progress.removeListeners(); } }