Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
520 lines
16 KiB
520 lines
16 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import { cancelEvent } from "../helpers/dom"; |
|
import appMediaPlaybackController from "../components/appMediaPlaybackController"; |
|
import { isAppleMobile } from "../helpers/userAgent"; |
|
import { isTouchSupported } from "../helpers/touchSupport"; |
|
import RangeSelector from "../components/rangeSelector"; |
|
import { onVideoLoad } from "../helpers/files"; |
|
|
|
export class MediaProgressLine extends RangeSelector { |
|
private filledLoad: HTMLDivElement; |
|
|
|
private stopAndScrubTimeout = 0; |
|
private progressRAF = 0; |
|
|
|
constructor(private media: HTMLAudioElement | HTMLVideoElement, private streamable = false) { |
|
super(1000 / 60 / 1000, 0, 0, 1); |
|
|
|
if(streamable) { |
|
this.filledLoad = document.createElement('div'); |
|
this.filledLoad.classList.add('progress-line__filled', 'progress-line__loaded'); |
|
this.container.prepend(this.filledLoad); |
|
//this.setLoadProgress(); |
|
} |
|
|
|
if(!media.paused || media.currentTime > 0) { |
|
this.onPlay(); |
|
} |
|
|
|
this.setSeekMax(); |
|
this.setListeners(); |
|
this.setHandlers({ |
|
onMouseDown: () => { |
|
//super.onMouseDown(e); |
|
|
|
//Таймер для того, чтобы стопать видео, если зажал мышку и не отпустил клик |
|
if(this.stopAndScrubTimeout) { // возможно лишнее |
|
clearTimeout(this.stopAndScrubTimeout); |
|
} |
|
|
|
this.stopAndScrubTimeout = window.setTimeout(() => { |
|
!this.media.paused && this.media.pause(); |
|
this.stopAndScrubTimeout = 0; |
|
}, 150); |
|
}, |
|
|
|
onMouseUp: () => { |
|
//super.onMouseUp(e); |
|
|
|
if(this.stopAndScrubTimeout) { |
|
clearTimeout(this.stopAndScrubTimeout); |
|
this.stopAndScrubTimeout = 0; |
|
} |
|
|
|
this.media.paused && this.media.play(); |
|
} |
|
}) |
|
} |
|
|
|
onLoadedData = () => { |
|
this.max = this.media.duration; |
|
this.seek.setAttribute('max', '' + this.max); |
|
}; |
|
|
|
onEnded = () => { |
|
this.setProgress(); |
|
}; |
|
|
|
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); |
|
}; |
|
|
|
onProgress = (e: Event) => { |
|
this.setLoadProgress(); |
|
}; |
|
|
|
protected scrub(e: MouseEvent) { |
|
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.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.streamable && this.media.removeEventListener('progress', this.onProgress); |
|
|
|
if(this.stopAndScrubTimeout) { |
|
clearTimeout(this.stopAndScrubTimeout); |
|
} |
|
|
|
if(this.progressRAF) { |
|
window.cancelAnimationFrame(this.progressRAF); |
|
} |
|
} |
|
} |
|
|
|
let lastVolume = 1, muted = !lastVolume; |
|
export default class VideoPlayer { |
|
public wrapper: HTMLDivElement; |
|
public progress: MediaProgressLine; |
|
private skin: string; |
|
|
|
/* private videoParent: HTMLElement; |
|
private videoWhichChild: number; */ |
|
|
|
constructor(public video: HTMLVideoElement, play = false, streamable = false, duration?: number) { |
|
this.wrapper = document.createElement('div'); |
|
this.wrapper.classList.add('ckin__player'); |
|
|
|
video.parentNode.insertBefore(this.wrapper, video); |
|
this.wrapper.appendChild(video); |
|
|
|
this.skin = video.dataset.ckin ?? 'default'; |
|
|
|
this.stylePlayer(duration); |
|
|
|
if(this.skin === 'default') { |
|
let 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<HTMLElement>; |
|
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 volumeDiv = document.createElement('div'); |
|
volumeDiv.classList.add('player-volume'); |
|
|
|
volumeDiv.innerHTML = ` |
|
<svg class="player-volume__icon" focusable="false" viewBox="0 0 24 24" aria-hidden="true"></svg> |
|
`; |
|
const volumeSvg = volumeDiv.firstElementChild as SVGSVGElement; |
|
|
|
volumeSvg.addEventListener('click', (e) => { |
|
cancelEvent(e); |
|
video.muted = !video.muted; |
|
}); |
|
|
|
const volumeProgress = new RangeSelector(0.01, 1, 0, 1); |
|
volumeProgress.setListeners(); |
|
volumeProgress.setHandlers({ |
|
onScrub: currentTime => { |
|
const value = Math.max(Math.min(currentTime, 1), 0); |
|
|
|
//console.log('volume scrub:', currentTime, value); |
|
|
|
video.muted = false; |
|
video.volume = value; |
|
} |
|
}); |
|
volumeDiv.append(volumeProgress.container); |
|
|
|
const setVolume = () => { |
|
const volume = video.volume; |
|
let d: string; |
|
if(!volume || video.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 { |
|
volumeSvg.innerHTML = `<path d="${d}"></path>`; |
|
} catch(err) {} |
|
|
|
if(!volumeProgress.mousedown) { |
|
volumeProgress.setProgress(video.muted ? 0 : volume); |
|
} |
|
}; |
|
|
|
// не вызовется повторно если на 1 установить 1 |
|
video.addEventListener('volumechange', () => { |
|
muted = video.muted; |
|
lastVolume = video.volume; |
|
setVolume(); |
|
}); |
|
|
|
video.volume = lastVolume; |
|
video.muted = muted; |
|
|
|
setVolume(); |
|
|
|
// volume end |
|
|
|
const leftControls = player.querySelector('.left-controls'); |
|
leftControls.insertBefore(volumeDiv, timeElapsed.parentElement); |
|
|
|
Array.from(toggle).forEach((button) => { |
|
return button.addEventListener('click', () => { |
|
this.togglePlay(); |
|
}); |
|
}); |
|
|
|
video.addEventListener('click', () => { |
|
if(!isTouchSupported) { |
|
this.togglePlay(); |
|
return; |
|
} |
|
}); |
|
|
|
if(isTouchSupported) { |
|
let showControlsTimeout = 0; |
|
|
|
const t = () => { |
|
showControlsTimeout = window.setTimeout(() => { |
|
showControlsTimeout = 0; |
|
player.classList.remove('show-controls'); |
|
}, 3e3); |
|
}; |
|
|
|
player.addEventListener('click', () => { |
|
if(showControlsTimeout) { |
|
clearTimeout(showControlsTimeout); |
|
} else { |
|
player.classList.add('show-controls'); |
|
} |
|
|
|
t(); |
|
}); |
|
|
|
player.addEventListener('touchstart', () => { |
|
player.classList.add('show-controls'); |
|
clearTimeout(showControlsTimeout); |
|
}); |
|
|
|
player.addEventListener('touchend', () => { |
|
if(player.classList.contains('is-playing')) { |
|
t(); |
|
} |
|
}); |
|
} |
|
|
|
/* player.addEventListener('click', (e) => { |
|
if(e.target !== player) { |
|
return; |
|
} |
|
|
|
this.togglePlay(); |
|
}); */ |
|
|
|
/* video.addEventListener('play', () => { |
|
}); */ |
|
|
|
video.addEventListener('dblclick', () => { |
|
if(isTouchSupported) { |
|
return; |
|
} |
|
|
|
return this.toggleFullScreen(fullScreenButton); |
|
}) |
|
|
|
fullScreenButton.addEventListener('click', (e) => { |
|
return this.toggleFullScreen(fullScreenButton); |
|
}); |
|
|
|
'webkitfullscreenchange mozfullscreenchange fullscreenchange MSFullscreenChange'.split(' ').forEach(eventName => { |
|
player.addEventListener(eventName, this.onFullScreen, false); |
|
}); |
|
|
|
video.addEventListener('timeupdate', () => { |
|
timeElapsed.innerHTML = String(video.currentTime | 0).toHHMMSS(); |
|
}); |
|
} |
|
|
|
video.addEventListener('play', () => { |
|
this.wrapper.classList.add('is-playing'); |
|
}); |
|
|
|
video.addEventListener('pause', () => { |
|
this.wrapper.classList.remove('is-playing'); |
|
}); |
|
|
|
if(video.duration || initDuration) { |
|
timeDuration.innerHTML = String(Math.round(video.duration || initDuration)).toHHMMSS(); |
|
} else { |
|
onVideoLoad(video).then(() => { |
|
timeDuration.innerHTML = String(Math.round(video.duration)).toHHMMSS(); |
|
}); |
|
} |
|
} |
|
|
|
public togglePlay(stop?: boolean) { |
|
//console.log('video togglePlay:', stop, this.video.paused); |
|
|
|
if(stop) { |
|
this.video.pause(); |
|
this.wrapper.classList.remove('is-playing'); |
|
return; |
|
} else if(stop === false) { |
|
this.video.play(); |
|
this.wrapper.classList.add('is-playing'); |
|
return; |
|
} |
|
|
|
this.video[this.video.paused ? 'play' : 'pause'](); |
|
//this.wrapper.classList.toggle('is-playing', !this.video.paused); |
|
} |
|
|
|
private buildControls() { |
|
const skin = this.skin; |
|
if(skin === 'default') { |
|
return ` |
|
<button class="${skin}__button--big toggle tgico" title="Toggle Play"></button> |
|
<div class="${skin}__gradient-bottom ckin__controls"></div> |
|
<div class="${skin}__controls ckin__controls"> |
|
<div class="bottom-controls"> |
|
<div class="left-controls"> |
|
<button class="${skin}__button toggle tgico" title="Toggle Video"></button> |
|
<div class="time"> |
|
<time id="time-elapsed">0:00</time> |
|
<span> / </span> |
|
<time id="time-duration">0:00</time> |
|
</div> |
|
</div> |
|
<div class="right-controls"> |
|
<button class="${skin}__button fullscreen tgico-fullscreen" title="Full Screen"></button> |
|
</div> |
|
</div> |
|
</div>`; |
|
} |
|
} |
|
|
|
public static isFullScreen(): boolean { |
|
// @ts-ignore |
|
return !!(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement); |
|
} |
|
|
|
public toggleFullScreen(fullScreenButton: HTMLElement) { |
|
// alternative standard method |
|
const player = this.wrapper; |
|
|
|
// * https://caniuse.com/#feat=fullscreen |
|
if(isAppleMobile) { |
|
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'); |
|
} |
|
} |
|
|
|
onFullScreen = () => { |
|
// @ts-ignore |
|
const isFullscreenNow = document.webkitFullscreenElement !== null; |
|
if(!isFullscreenNow) { |
|
this.wrapper.classList.remove('ckin__fullscreen'); |
|
} else { |
|
} |
|
}; |
|
}
|
|
|