2021-04-08 15:52:31 +02:00
|
|
|
/*
|
|
|
|
* https://github.com/morethanwords/tweb
|
|
|
|
* Copyright (C) 2019-2021 Eduard Kuzmenko
|
|
|
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE
|
|
|
|
*/
|
|
|
|
|
2022-08-04 08:49:54 +02:00
|
|
|
import appMediaPlaybackController from '../components/appMediaPlaybackController';
|
|
|
|
import {IS_APPLE_MOBILE, IS_MOBILE} from '../environment/userAgent';
|
|
|
|
import IS_TOUCH_SUPPORTED from '../environment/touchSupport';
|
|
|
|
import cancelEvent from '../helpers/dom/cancelEvent';
|
|
|
|
import ListenerSetter, {Listener} from '../helpers/listenerSetter';
|
2024-03-12 18:00:59 +01:00
|
|
|
import {ButtonMenuItemOptionsVerifiable, ButtonMenuSync} from '../components/buttonMenu';
|
|
|
|
import ButtonMenuToggle, {ButtonMenuToggleHandler} from '../components/buttonMenuToggle';
|
2022-08-04 08:49:54 +02:00
|
|
|
import ControlsHover from '../helpers/dom/controlsHover';
|
2024-03-12 18:00:59 +01:00
|
|
|
import {addFullScreenListener, cancelFullScreen, getFullScreenElement, isFullScreen, requestFullScreen} from '../helpers/dom/fullScreen';
|
2022-08-04 08:49:54 +02:00
|
|
|
import toHHMMSS from '../helpers/string/toHHMMSS';
|
|
|
|
import MediaProgressLine from '../components/mediaProgressLine';
|
|
|
|
import VolumeSelector from '../components/volumeSelector';
|
|
|
|
import debounce from '../helpers/schedulers/debounce';
|
|
|
|
import overlayCounter from '../helpers/overlayCounter';
|
|
|
|
import onMediaLoad from '../helpers/onMediaLoad';
|
2022-08-09 17:35:11 +02:00
|
|
|
import {attachClickEvent} from '../helpers/dom/clickEvent';
|
2023-09-06 22:28:19 +02:00
|
|
|
import safePlay from '../helpers/dom/safePlay';
|
|
|
|
import ButtonIcon from '../components/buttonIcon';
|
|
|
|
import Button from '../components/button';
|
|
|
|
import Icon from '../components/icon';
|
2023-09-12 14:02:33 +02:00
|
|
|
import setCurrentTime from '../helpers/dom/setCurrentTime';
|
2024-03-12 18:00:59 +01:00
|
|
|
import {i18n} from './langPack';
|
|
|
|
import {numberThousandSplitterForWatching} from '../helpers/number/numberThousandSplitter';
|
|
|
|
import createCanvasStream from '../helpers/canvas/createCanvasStream';
|
2024-05-18 19:55:12 +02:00
|
|
|
import simulateEvent from '../helpers/dom/dispatchEvent';
|
|
|
|
import indexOfAndSplice from '../helpers/array/indexOfAndSplice';
|
2021-10-05 22:40:07 +02:00
|
|
|
|
2024-04-21 15:07:59 +02:00
|
|
|
export const PlaybackRateButton = (options: {
|
|
|
|
onPlaybackRateMenuToggle?: (open: boolean) => void,
|
|
|
|
direction: string
|
|
|
|
}) => {
|
|
|
|
const PLAYBACK_RATES = [0.5, 1, 1.5, 2];
|
|
|
|
const PLAYBACK_RATES_ICONS: Icon[] = ['playback_05', 'playback_1x', 'playback_15', 'playback_2x'];
|
|
|
|
const button = ButtonIcon(` btn-menu-toggle`, {noRipple: true});
|
|
|
|
|
|
|
|
const setIcon = () => {
|
|
|
|
const playbackRateButton = button;
|
|
|
|
|
|
|
|
let idx = PLAYBACK_RATES.indexOf(appMediaPlaybackController.playbackRate);
|
|
|
|
if(idx === -1) idx = PLAYBACK_RATES.indexOf(1);
|
|
|
|
|
|
|
|
const icon = Icon(PLAYBACK_RATES_ICONS[idx]);
|
|
|
|
if(playbackRateButton.firstElementChild) {
|
|
|
|
playbackRateButton.firstElementChild.replaceWith(icon);
|
|
|
|
} else {
|
|
|
|
playbackRateButton.append(icon);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const setBtnMenuToggle = () => {
|
|
|
|
const buttons = PLAYBACK_RATES.map((rate, idx) => {
|
|
|
|
const buttonOptions: Parameters<typeof ButtonMenuSync>[0]['buttons'][0] = {
|
|
|
|
// icon: PLAYBACK_RATES_ICONS[idx],
|
|
|
|
regularText: rate + 'x',
|
|
|
|
onClick: () => {
|
|
|
|
appMediaPlaybackController.playbackRate = rate;
|
|
|
|
}
|
|
|
|
};
|
2022-02-09 14:34:34 +01:00
|
|
|
|
2024-04-21 15:07:59 +02:00
|
|
|
return buttonOptions;
|
|
|
|
});
|
|
|
|
const btnMenu = ButtonMenuSync({buttons});
|
|
|
|
btnMenu.classList.add(options.direction, 'playback-rate-menu');
|
|
|
|
ButtonMenuToggleHandler({
|
|
|
|
el: button,
|
|
|
|
onOpen: options.onPlaybackRateMenuToggle ? () => {
|
|
|
|
options.onPlaybackRateMenuToggle(true);
|
|
|
|
} : undefined,
|
|
|
|
onClose: options.onPlaybackRateMenuToggle ? () => {
|
|
|
|
options.onPlaybackRateMenuToggle(false);
|
|
|
|
} : undefined
|
|
|
|
});
|
|
|
|
setIcon();
|
|
|
|
button.append(btnMenu);
|
|
|
|
};
|
|
|
|
|
|
|
|
const addRate = (add: number) => {
|
|
|
|
const playbackRate = appMediaPlaybackController.playbackRate;
|
|
|
|
const idx = PLAYBACK_RATES.indexOf(playbackRate);
|
|
|
|
const nextIdx = idx + add;
|
|
|
|
if(nextIdx >= 0 && nextIdx < PLAYBACK_RATES.length) {
|
|
|
|
appMediaPlaybackController.playbackRate = PLAYBACK_RATES[nextIdx];
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const isMenuOpen = () => {
|
|
|
|
return button.classList.contains('menu-open');
|
|
|
|
};
|
|
|
|
|
|
|
|
setBtnMenuToggle();
|
|
|
|
return {element: button, setIcon, addRate, isMenuOpen};
|
|
|
|
};
|
|
|
|
|
|
|
|
export default class VideoPlayer extends ControlsHover {
|
2024-03-12 18:00:59 +01:00
|
|
|
public video: HTMLVideoElement;
|
2024-05-18 19:55:12 +02:00
|
|
|
protected wrapper: HTMLElement;
|
2021-12-11 17:37:08 +01:00
|
|
|
protected progress: MediaProgressLine;
|
|
|
|
protected skin: 'default';
|
2024-03-12 18:00:59 +01:00
|
|
|
protected live: boolean;
|
2020-04-25 03:17:50 +02:00
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
protected listenerSetter: ListenerSetter;
|
2024-04-21 15:07:59 +02:00
|
|
|
protected playbackRateButton: ReturnType<typeof PlaybackRateButton>;
|
2022-04-15 20:04:56 +02:00
|
|
|
protected pipButton: HTMLElement;
|
2024-03-12 18:00:59 +01:00
|
|
|
protected liveMenuButton: HTMLElement;
|
2023-09-06 22:28:19 +02:00
|
|
|
protected toggles: HTMLElement[];
|
2024-05-18 19:55:12 +02:00
|
|
|
protected mainToggle: HTMLElement;
|
|
|
|
protected controls: HTMLElement;
|
|
|
|
protected gradient: HTMLElement;
|
2024-03-12 18:00:59 +01:00
|
|
|
public liveEl: HTMLElement;
|
2021-07-01 04:30:05 +02:00
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
/* protected videoParent: HTMLElement;
|
|
|
|
protected videoWhichChild: number; */
|
2021-08-21 00:30:41 +02:00
|
|
|
|
2024-04-21 15:07:59 +02:00
|
|
|
protected onPlaybackRateMenuToggle?: (open: boolean) => void;
|
2022-04-15 20:04:56 +02:00
|
|
|
protected onPip?: (pip: boolean) => void;
|
2022-04-17 00:39:01 +02:00
|
|
|
protected onPipClose?: () => void;
|
2024-05-18 19:55:12 +02:00
|
|
|
protected onVolumeChange: VolumeSelector['onVolumeChange'];
|
|
|
|
protected onFullScreen: (active: boolean) => void;
|
|
|
|
protected onFullScreenToPip: () => void;
|
2022-02-11 15:36:00 +01:00
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
protected canPause: boolean;
|
|
|
|
protected canSeek: boolean;
|
|
|
|
|
|
|
|
protected _inPip = false;
|
|
|
|
|
|
|
|
protected _width: number;
|
|
|
|
protected _height: number;
|
|
|
|
|
|
|
|
protected emptyPipVideo: HTMLVideoElement;
|
|
|
|
protected debouncedPip: (pip: boolean) => void;
|
|
|
|
protected debouncePipTime: number;
|
|
|
|
public emptyPipVideoSource: CanvasImageSource;
|
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
protected listenKeyboardEvents: 'always' | 'fullscreen';
|
|
|
|
protected hadContainer: boolean;
|
|
|
|
protected useGlobalVolume: VolumeSelector['useGlobalVolume'];
|
|
|
|
public volumeSelector: VolumeSelector;
|
|
|
|
protected isPlaying: boolean;
|
|
|
|
protected shouldEnableSoundOnClick: () => boolean;
|
|
|
|
|
2023-03-06 15:56:03 +01:00
|
|
|
constructor({
|
|
|
|
video,
|
2024-05-18 19:55:12 +02:00
|
|
|
container,
|
2023-03-06 15:56:03 +01:00
|
|
|
play = false,
|
|
|
|
streamable = false,
|
|
|
|
duration,
|
2024-03-12 18:00:59 +01:00
|
|
|
live,
|
|
|
|
width,
|
|
|
|
height,
|
2024-04-21 15:07:59 +02:00
|
|
|
onPlaybackRateMenuToggle,
|
2023-03-06 15:56:03 +01:00
|
|
|
onPip,
|
2024-05-18 19:55:12 +02:00
|
|
|
onPipClose,
|
|
|
|
listenKeyboardEvents,
|
|
|
|
useGlobalVolume,
|
|
|
|
onVolumeChange,
|
|
|
|
onFullScreen,
|
|
|
|
onFullScreenToPip,
|
|
|
|
shouldEnableSoundOnClick
|
2023-03-06 15:56:03 +01:00
|
|
|
}: {
|
2022-08-04 08:49:54 +02:00
|
|
|
video: HTMLVideoElement,
|
2024-05-18 19:55:12 +02:00
|
|
|
container?: HTMLElement,
|
2022-08-04 08:49:54 +02:00
|
|
|
play?: boolean,
|
|
|
|
streamable?: boolean,
|
2022-02-11 15:36:00 +01:00
|
|
|
duration?: number,
|
2024-03-12 18:00:59 +01:00
|
|
|
live?: boolean,
|
|
|
|
width?: number,
|
|
|
|
height?: number,
|
2024-04-21 15:07:59 +02:00
|
|
|
onPlaybackRateMenuToggle?: VideoPlayer['onPlaybackRateMenuToggle'],
|
2022-04-17 00:39:01 +02:00
|
|
|
onPip?: VideoPlayer['onPip'],
|
2024-05-18 19:55:12 +02:00
|
|
|
onPipClose?: VideoPlayer['onPipClose'],
|
|
|
|
listenKeyboardEvents?: VideoPlayer['listenKeyboardEvents'],
|
|
|
|
useGlobalVolume?: VolumeSelector['useGlobalVolume'],
|
|
|
|
onVolumeChange?: VideoPlayer['onVolumeChange'],
|
|
|
|
onFullScreen?: (active: boolean) => void,
|
|
|
|
onFullScreenToPip?: VideoPlayer['onFullScreenToPip'],
|
|
|
|
shouldEnableSoundOnClick?: VideoPlayer['shouldEnableSoundOnClick']
|
2022-02-11 15:36:00 +01:00
|
|
|
}) {
|
2021-12-11 17:37:08 +01:00
|
|
|
super();
|
2021-08-11 15:39:49 +02:00
|
|
|
|
2022-02-11 15:36:00 +01:00
|
|
|
this.video = video;
|
2024-03-12 18:00:59 +01:00
|
|
|
this.video.classList.add('ckin__video');
|
2024-05-18 19:55:12 +02:00
|
|
|
this.wrapper = container ?? document.createElement('div');
|
2020-04-25 03:17:50 +02:00
|
|
|
this.wrapper.classList.add('ckin__player');
|
2024-03-12 18:00:59 +01:00
|
|
|
this.live = live;
|
|
|
|
this.canPause = !live;
|
|
|
|
this.canSeek = !live;
|
|
|
|
this._width = width;
|
|
|
|
this._height = height;
|
2020-04-25 03:17:50 +02:00
|
|
|
|
2024-04-21 15:07:59 +02:00
|
|
|
this.onPlaybackRateMenuToggle = onPlaybackRateMenuToggle;
|
2022-04-15 20:04:56 +02:00
|
|
|
this.onPip = onPip;
|
2022-04-17 00:39:01 +02:00
|
|
|
this.onPipClose = onPipClose;
|
2024-05-18 19:55:12 +02:00
|
|
|
this.onVolumeChange = onVolumeChange;
|
|
|
|
this.onFullScreen = onFullScreen;
|
|
|
|
this.onFullScreenToPip = onFullScreenToPip;
|
|
|
|
|
|
|
|
this.listenKeyboardEvents = listenKeyboardEvents;
|
|
|
|
this.hadContainer = !!container;
|
|
|
|
this.useGlobalVolume = useGlobalVolume;
|
|
|
|
this.shouldEnableSoundOnClick = shouldEnableSoundOnClick;
|
2022-02-11 15:36:00 +01:00
|
|
|
|
2021-07-01 04:30:05 +02:00
|
|
|
this.listenerSetter = new ListenerSetter();
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
this.setup({
|
2022-08-04 08:49:54 +02:00
|
|
|
element: this.wrapper,
|
|
|
|
listenerSetter: this.listenerSetter,
|
2021-12-11 17:37:08 +01:00
|
|
|
canHideControls: () => {
|
2024-04-21 15:07:59 +02:00
|
|
|
return !this.video.paused && (!this.playbackRateButton || !this.playbackRateButton.isMenuOpen());
|
2021-12-11 17:37:08 +01:00
|
|
|
},
|
2022-02-11 15:36:00 +01:00
|
|
|
showOnLeaveToClassName: 'media-viewer-caption',
|
|
|
|
ignoreClickClassName: 'ckin__controls'
|
2021-12-11 17:37:08 +01:00
|
|
|
});
|
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
if(!this.hadContainer) {
|
|
|
|
video.parentNode.insertBefore(this.wrapper, video);
|
|
|
|
this.wrapper.appendChild(video);
|
|
|
|
}
|
2020-04-25 03:17:50 +02:00
|
|
|
|
2021-07-20 00:24:29 +02:00
|
|
|
this.skin = 'default';
|
2020-04-25 03:17:50 +02:00
|
|
|
|
2021-02-01 03:09:18 +01:00
|
|
|
this.stylePlayer(duration);
|
2020-04-25 03:17:50 +02:00
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
if(this.skin === 'default' && !live) {
|
2024-05-18 19:55:12 +02:00
|
|
|
const controls = this.controls = this.wrapper.querySelector('.default__controls.ckin__controls') as HTMLDivElement;
|
|
|
|
this.gradient = this.controls.previousElementSibling as HTMLElement;
|
2023-01-31 16:14:54 +01:00
|
|
|
this.progress = new MediaProgressLine({
|
|
|
|
onSeekStart: () => {
|
|
|
|
this.wrapper.classList.add('is-seeking');
|
|
|
|
},
|
|
|
|
onSeekEnd: () => {
|
|
|
|
this.wrapper.classList.remove('is-seeking');
|
|
|
|
}
|
|
|
|
});
|
2023-03-06 15:56:03 +01:00
|
|
|
this.progress.setMedia({
|
|
|
|
media: video,
|
|
|
|
streamable,
|
|
|
|
duration
|
|
|
|
});
|
2020-04-25 03:17:50 +02:00
|
|
|
controls.prepend(this.progress.container);
|
|
|
|
}
|
|
|
|
|
2020-08-30 12:43:57 +02:00
|
|
|
if(play/* && video.paused */) {
|
2024-03-12 18:00:59 +01:00
|
|
|
video.play().catch((err: Error) => {
|
2021-02-04 01:30:23 +01:00
|
|
|
if(err.name === 'NotAllowedError') {
|
2020-08-28 22:43:55 +02:00
|
|
|
video.muted = true;
|
|
|
|
video.autoplay = true;
|
2023-09-06 22:28:19 +02:00
|
|
|
safePlay(video);
|
2020-08-28 22:43:55 +02:00
|
|
|
}
|
2020-08-30 13:06:24 +02:00
|
|
|
}).finally(() => { // due to autoplay, play will not call
|
2023-09-06 22:28:19 +02:00
|
|
|
this.setIsPlaing(!this.video.paused);
|
2020-08-28 22:43:55 +02:00
|
|
|
});
|
2024-05-18 19:55:12 +02:00
|
|
|
} else {
|
|
|
|
this.setIsPlaing(!this.video.paused);
|
2020-04-25 03:17:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
public get width() {
|
|
|
|
return this.video.videoWidth || this._width;
|
|
|
|
}
|
|
|
|
|
|
|
|
public get height() {
|
|
|
|
return this.video.videoHeight || this._height;
|
|
|
|
}
|
|
|
|
|
2023-09-06 22:28:19 +02:00
|
|
|
private setIsPlaing(isPlaying: boolean) {
|
2024-05-18 19:55:12 +02:00
|
|
|
if(this.isPlaying === isPlaying) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.isPlaying = isPlaying;
|
|
|
|
|
|
|
|
if(this.live && !isPlaying) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-09-06 22:28:19 +02:00
|
|
|
this.wrapper.classList.toggle('is-playing', isPlaying);
|
|
|
|
this.toggles.forEach((toggle) => {
|
|
|
|
toggle.replaceChildren(Icon(isPlaying ? 'pause' : 'play'));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-02-01 03:09:18 +01:00
|
|
|
private stylePlayer(initDuration: number) {
|
2024-03-12 18:00:59 +01:00
|
|
|
const {wrapper, video, skin, listenerSetter, live} = this;
|
2020-04-25 03:17:50 +02:00
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
wrapper.classList.add(skin);
|
2024-03-12 18:00:59 +01:00
|
|
|
if(live) wrapper.classList.add(`${skin}-live`);
|
2022-08-04 08:49:54 +02:00
|
|
|
|
2020-08-26 18:14:23 +02:00
|
|
|
const html = this.buildControls();
|
2021-12-11 17:37:08 +01:00
|
|
|
wrapper.insertAdjacentHTML('beforeend', html);
|
2020-10-28 19:20:01 +01:00
|
|
|
let timeDuration: HTMLElement;
|
2022-08-04 08:49:54 +02:00
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
const onPlayCallbacks: (() => void)[] = [], onPauseCallbacks: (() => void)[] = [];
|
2020-04-25 03:17:50 +02:00
|
|
|
if(skin === 'default') {
|
2024-03-12 18:00:59 +01:00
|
|
|
if(this.canPause) {
|
2024-05-18 19:55:12 +02:00
|
|
|
const mainToggle = this.mainToggle = Button(`${skin}__button--big toggle`, {noRipple: true, icon: 'play'});
|
2024-03-12 18:00:59 +01:00
|
|
|
wrapper.firstElementChild.after(mainToggle);
|
|
|
|
}
|
2023-09-06 22:28:19 +02:00
|
|
|
|
|
|
|
const leftControls = wrapper.querySelector('.left-controls') as HTMLElement;
|
2024-03-12 18:00:59 +01:00
|
|
|
if(live) {
|
|
|
|
this.toggles = [];
|
|
|
|
} else {
|
|
|
|
const leftToggle = ButtonIcon(` ${skin}__button toggle`, {noRipple: true});
|
|
|
|
leftControls.prepend(leftToggle);
|
|
|
|
this.toggles = [leftToggle];
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
2023-09-06 22:28:19 +02:00
|
|
|
const rightControls = wrapper.querySelector('.right-controls') as HTMLElement;
|
2024-03-12 18:00:59 +01:00
|
|
|
if(!live) {
|
2024-04-21 15:07:59 +02:00
|
|
|
this.playbackRateButton = PlaybackRateButton({direction: 'top-left', onPlaybackRateMenuToggle: this.onPlaybackRateMenuToggle});
|
|
|
|
this.playbackRateButton.element.classList.add(`${skin}__button`);
|
2024-03-12 18:00:59 +01:00
|
|
|
}
|
2023-09-06 22:28:19 +02:00
|
|
|
if(!IS_MOBILE && document.pictureInPictureEnabled) {
|
|
|
|
this.pipButton = ButtonIcon(`pip ${skin}__button`, {noRipple: true});
|
|
|
|
}
|
|
|
|
const fullScreenButton = ButtonIcon(` ${skin}__button`, {noRipple: true});
|
2024-04-21 15:07:59 +02:00
|
|
|
rightControls.append(...[this.playbackRateButton?.element, this.pipButton, fullScreenButton].filter(Boolean));
|
2023-09-06 22:28:19 +02:00
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
const timeElapsed = wrapper.querySelector('.ckin__time-elapsed');
|
|
|
|
timeDuration = wrapper.querySelector('.ckin__time-duration') as HTMLElement;
|
2020-08-26 21:25:43 +02:00
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
const volumeSelector = this.volumeSelector = new VolumeSelector({
|
|
|
|
listenerSetter,
|
|
|
|
vertical: false,
|
|
|
|
media: video,
|
|
|
|
useGlobalVolume: this.useGlobalVolume,
|
|
|
|
onVolumeChange: this.onVolumeChange
|
|
|
|
});
|
2020-08-26 21:25:43 +02:00
|
|
|
|
2021-10-29 15:05:55 +02:00
|
|
|
volumeSelector.btn.classList.remove('btn-icon');
|
2024-03-12 18:00:59 +01:00
|
|
|
if(timeElapsed) {
|
|
|
|
timeElapsed.parentElement.before(volumeSelector.btn);
|
|
|
|
} else {
|
|
|
|
leftControls.lastElementChild.before(volumeSelector.btn);
|
|
|
|
}
|
2020-08-26 21:25:43 +02:00
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
this.toggles.forEach((button) => {
|
2022-07-26 07:22:46 +02:00
|
|
|
attachClickEvent(button, () => {
|
2020-04-25 03:17:50 +02:00
|
|
|
this.togglePlay();
|
2022-07-26 07:22:46 +02:00
|
|
|
}, {listenerSetter: this.listenerSetter});
|
2020-04-25 03:17:50 +02:00
|
|
|
});
|
2020-08-27 20:25:47 +02:00
|
|
|
|
2022-04-15 20:04:56 +02:00
|
|
|
if(this.pipButton) {
|
2024-03-12 18:00:59 +01:00
|
|
|
attachClickEvent(this.pipButton, this.requestPictureInPicture, {listenerSetter: this.listenerSetter});
|
2022-04-15 20:04:56 +02:00
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
this.debouncePipTime = 20;
|
|
|
|
this.debouncedPip = debounce(this._onPip, this.debouncePipTime, false, true);
|
2022-04-15 20:04:56 +02:00
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
this.addPipListeners(video);
|
2022-04-15 20:04:56 +02:00
|
|
|
}
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
if(!IS_TOUCH_SUPPORTED) {
|
2024-03-12 18:00:59 +01:00
|
|
|
if(this.canPause) {
|
|
|
|
attachClickEvent(video, () => {
|
2024-05-18 19:55:12 +02:00
|
|
|
if(this.checkInteraction()) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
this.togglePlay();
|
|
|
|
}, {listenerSetter: this.listenerSetter});
|
|
|
|
}
|
2021-08-18 21:02:46 +02:00
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
if(this.listenKeyboardEvents) listenerSetter.add(document)('keydown', (e: KeyboardEvent) => {
|
|
|
|
if(
|
|
|
|
overlayCounter.overlaysActive > 1 ||
|
|
|
|
document.pictureInPictureElement === video ||
|
|
|
|
(this.listenKeyboardEvents === 'fullscreen' && !this.isFullScreen())
|
|
|
|
) { // forward popup is active, etc
|
2021-08-11 15:39:49 +02:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-12-01 13:46:58 +01:00
|
|
|
const {key, code} = e;
|
2021-11-24 14:40:18 +01:00
|
|
|
|
2021-08-11 17:51:44 +02:00
|
|
|
let good = true;
|
2021-12-01 13:46:58 +01:00
|
|
|
if(code === 'KeyF') {
|
2021-12-11 17:37:08 +01:00
|
|
|
this.toggleFullScreen();
|
2021-12-01 13:46:58 +01:00
|
|
|
} else if(code === 'KeyM') {
|
2021-10-05 22:40:07 +02:00
|
|
|
appMediaPlaybackController.muted = !appMediaPlaybackController.muted;
|
2024-03-12 18:00:59 +01:00
|
|
|
} else if(code === 'Space' && this.canPause) {
|
2021-07-01 04:30:05 +02:00
|
|
|
this.togglePlay();
|
2024-03-12 18:00:59 +01:00
|
|
|
} else if(e.altKey && (code === 'Equal' || code === 'Minus') && this.canSeek) {
|
2022-02-09 14:34:34 +01:00
|
|
|
const add = code === 'Equal' ? 1 : -1;
|
2024-04-21 15:07:59 +02:00
|
|
|
this.playbackRateButton.addRate(add);
|
2024-03-12 18:00:59 +01:00
|
|
|
} else if(wrapper.classList.contains('ckin__fullscreen') && (key === 'ArrowLeft' || key === 'ArrowRight') && this.canSeek) {
|
2021-11-24 14:40:18 +01:00
|
|
|
if(key === 'ArrowLeft') appMediaPlaybackController.seekBackward({action: 'seekbackward'});
|
2021-10-28 13:46:48 +02:00
|
|
|
else appMediaPlaybackController.seekForward({action: 'seekforward'});
|
2021-08-11 17:51:44 +02:00
|
|
|
} else {
|
|
|
|
good = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(good) {
|
|
|
|
cancelEvent(e);
|
|
|
|
return false;
|
2020-09-01 14:53:46 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2020-04-25 03:17:50 +02:00
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
listenerSetter.add(video)('dblclick', () => {
|
2021-09-26 15:59:10 +02:00
|
|
|
if(!IS_TOUCH_SUPPORTED) {
|
2021-12-11 17:37:08 +01:00
|
|
|
this.toggleFullScreen();
|
2020-09-01 14:53:46 +02:00
|
|
|
}
|
2021-07-01 04:30:05 +02:00
|
|
|
});
|
2020-09-01 14:53:46 +02:00
|
|
|
|
2022-07-26 07:22:46 +02:00
|
|
|
attachClickEvent(fullScreenButton, () => {
|
2021-12-11 17:37:08 +01:00
|
|
|
this.toggleFullScreen();
|
2022-07-26 07:22:46 +02:00
|
|
|
}, {listenerSetter: this.listenerSetter});
|
2020-04-25 03:17:50 +02:00
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
addFullScreenListener(wrapper, () => this._onFullScreen(fullScreenButton), listenerSetter);
|
|
|
|
this._onFullScreen(fullScreenButton, true);
|
2020-10-28 19:20:01 +01:00
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
if(timeElapsed) {
|
|
|
|
listenerSetter.add(video)('timeupdate', () => {
|
2024-05-18 19:55:12 +02:00
|
|
|
if(!video.paused && !this.isPlaying) {
|
|
|
|
console.warn('video: fixing missing play event');
|
|
|
|
simulateEvent(video, 'play');
|
|
|
|
}
|
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
timeElapsed.textContent = toHHMMSS(video.currentTime | 0);
|
|
|
|
});
|
|
|
|
}
|
2021-07-01 04:30:05 +02:00
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
const onPlay = () => {
|
2021-12-11 17:37:08 +01:00
|
|
|
wrapper.classList.add('played');
|
2021-08-11 15:39:49 +02:00
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
if(!IS_TOUCH_SUPPORTED && !live) {
|
|
|
|
onPlayCallbacks.push(() => {
|
|
|
|
this.hideControls(true);
|
2022-02-11 15:36:00 +01:00
|
|
|
});
|
|
|
|
}
|
2022-02-09 14:48:45 +01:00
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
indexOfAndSplice(onPlayCallbacks, onPlay);
|
|
|
|
};
|
|
|
|
|
|
|
|
if(this.hadContainer) {
|
|
|
|
onPlay();
|
|
|
|
}
|
|
|
|
|
|
|
|
onPlayCallbacks.push(onPlay);
|
|
|
|
onPauseCallbacks.push(() => {
|
2021-08-21 00:30:41 +02:00
|
|
|
this.showControls(false);
|
2021-08-11 15:39:49 +02:00
|
|
|
});
|
2022-02-09 14:34:34 +01:00
|
|
|
|
2022-06-17 18:01:43 +02:00
|
|
|
listenerSetter.add(appMediaPlaybackController)('playbackParams', () => {
|
2024-04-21 15:07:59 +02:00
|
|
|
this.playbackRateButton.setIcon();
|
2022-02-09 14:34:34 +01:00
|
|
|
});
|
2024-03-12 18:00:59 +01:00
|
|
|
|
|
|
|
if(live) {
|
|
|
|
this.liveEl = i18n('Rtmp.MediaViewer.Live');
|
|
|
|
this.liveEl.classList.add('controls-live');
|
|
|
|
leftControls.prepend(this.liveEl);
|
|
|
|
}
|
2020-04-25 03:17:50 +02:00
|
|
|
}
|
2020-08-29 13:45:37 +02:00
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
listenerSetter.add(video)('play', () => {
|
2023-09-06 22:28:19 +02:00
|
|
|
this.setIsPlaing(true);
|
2024-05-18 19:55:12 +02:00
|
|
|
onPlayCallbacks.forEach((cb) => cb());
|
2020-08-29 13:45:37 +02:00
|
|
|
});
|
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
listenerSetter.add(video)('pause', () => {
|
|
|
|
this.setIsPlaing(false);
|
|
|
|
onPauseCallbacks.forEach((cb) => cb());
|
|
|
|
});
|
2024-03-12 18:00:59 +01:00
|
|
|
|
|
|
|
if(timeDuration) {
|
|
|
|
if(video.duration || initDuration) {
|
|
|
|
timeDuration.textContent = toHHMMSS(Math.round(video.duration || initDuration));
|
|
|
|
} else {
|
|
|
|
onMediaLoad(video).then(() => {
|
|
|
|
timeDuration.textContent = toHHMMSS(Math.round(video.duration));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2020-04-25 03:17:50 +02:00
|
|
|
}
|
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
protected checkInteraction() {
|
|
|
|
if(this.shouldEnableSoundOnClick?.()) {
|
|
|
|
this.volumeSelector.setVolume({volume: 1, muted: false});
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
protected _onPip = (pip: boolean) => {
|
|
|
|
this._inPip = pip;
|
|
|
|
this.wrapper.style.visibility = pip ? 'hidden': '';
|
|
|
|
this.onPip?.(pip);
|
|
|
|
};
|
|
|
|
|
|
|
|
protected onEnterPictureInPictureLeave = (e: Event) => {
|
|
|
|
const onPause = () => {
|
|
|
|
clearTimeout(timeout);
|
|
|
|
this.onPipClose?.();
|
|
|
|
};
|
|
|
|
const listener = this.listenerSetter.add(e.target)('pause', onPause, {once: true}) as any as Listener;
|
|
|
|
const timeout = setTimeout(() => {
|
|
|
|
this.listenerSetter.remove(listener);
|
|
|
|
}, this.debouncePipTime);
|
|
|
|
};
|
|
|
|
|
|
|
|
protected onEnterPictureInPicture = (e: Event) => {
|
|
|
|
this.debouncedPip(true);
|
|
|
|
this.listenerSetter.add(e.target)('leavepictureinpicture', this.onEnterPictureInPictureLeave, {once: true});
|
|
|
|
};
|
|
|
|
|
|
|
|
protected onLeavePictureInPicture = () => {
|
|
|
|
this.debouncedPip(false);
|
|
|
|
};
|
|
|
|
|
|
|
|
protected addPipListeners(video: HTMLVideoElement) {
|
|
|
|
this.listenerSetter.add(video)('enterpictureinpicture', this.onEnterPictureInPicture);
|
|
|
|
this.listenerSetter.add(video)('leavepictureinpicture', this.onLeavePictureInPicture);
|
|
|
|
}
|
|
|
|
|
|
|
|
public requestPictureInPicture = async() => {
|
|
|
|
if(this.video.duration) {
|
2024-05-18 19:55:12 +02:00
|
|
|
if(this.isFullScreen()) {
|
|
|
|
this.onFullScreenToPip?.();
|
|
|
|
}
|
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
this.video.requestPictureInPicture();
|
2024-05-18 19:55:12 +02:00
|
|
|
this.checkInteraction();
|
2024-03-12 18:00:59 +01:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!this.emptyPipVideo) {
|
|
|
|
const {width, height} = this;
|
|
|
|
this.emptyPipVideo = document.createElement('video');
|
2024-03-15 13:47:32 +01:00
|
|
|
this.emptyPipVideo.autoplay = true;
|
|
|
|
this.emptyPipVideo.muted = true;
|
|
|
|
this.emptyPipVideo.playsInline = true;
|
|
|
|
this.emptyPipVideo.style.position = 'absolute';
|
|
|
|
this.emptyPipVideo.style.visibility = 'hidden';
|
|
|
|
document.body.prepend(this.emptyPipVideo);
|
2024-03-12 18:00:59 +01:00
|
|
|
this.emptyPipVideo.srcObject = createCanvasStream({width, height, image: this.emptyPipVideoSource});
|
|
|
|
this.addPipListeners(this.emptyPipVideo);
|
|
|
|
}
|
|
|
|
|
|
|
|
await onMediaLoad(this.emptyPipVideo);
|
|
|
|
this.emptyPipVideo.requestPictureInPicture();
|
|
|
|
|
|
|
|
onMediaLoad(this.video).then(() => {
|
|
|
|
if(document.pictureInPictureElement === this.emptyPipVideo) {
|
|
|
|
document.exitPictureInPicture();
|
|
|
|
this.video.requestPictureInPicture();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-03-03 16:47:02 +01:00
|
|
|
protected togglePlay(isPaused = this.video.paused) {
|
2023-02-01 13:47:24 +01:00
|
|
|
this.video[isPaused ? 'play' : 'pause']();
|
2020-04-25 03:17:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
private buildControls() {
|
2020-08-26 18:14:23 +02:00
|
|
|
const skin = this.skin;
|
2024-03-12 18:00:59 +01:00
|
|
|
|
2020-04-25 03:17:50 +02:00
|
|
|
if(skin === 'default') {
|
2024-03-12 18:00:59 +01:00
|
|
|
const time = this.live ? `
|
|
|
|
<span class="left-controls-watching"></span>
|
|
|
|
` : `
|
2024-05-18 19:55:12 +02:00
|
|
|
<time class="ckin__time-elapsed">0:00</time>
|
2024-03-12 18:00:59 +01:00
|
|
|
<span> / </span>
|
2024-05-18 19:55:12 +02:00
|
|
|
<time class="ckin__time-duration">0:00</time>
|
2024-03-12 18:00:59 +01:00
|
|
|
`
|
|
|
|
|
2020-08-26 21:25:43 +02:00
|
|
|
return `
|
2020-08-26 18:14:23 +02:00
|
|
|
<div class="${skin}__gradient-bottom ckin__controls"></div>
|
2024-05-18 19:55:12 +02:00
|
|
|
<div class="${skin}__controls ckin__controls">
|
|
|
|
<div class="bottom-controls night">
|
2020-08-26 18:14:23 +02:00
|
|
|
<div class="left-controls">
|
2024-05-18 19:55:12 +02:00
|
|
|
<div class="ckin__time">
|
2024-03-12 18:00:59 +01:00
|
|
|
${time}
|
2020-08-26 18:14:23 +02:00
|
|
|
</div>
|
|
|
|
</div>
|
2023-09-06 22:28:19 +02:00
|
|
|
<div class="right-controls"></div>
|
2020-08-26 18:14:23 +02:00
|
|
|
</div>
|
2020-08-26 21:25:43 +02:00
|
|
|
</div>`;
|
2020-04-25 03:17:50 +02:00
|
|
|
}
|
|
|
|
}
|
2020-09-01 14:53:46 +02:00
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
public cancelFullScreen() {
|
|
|
|
if(getFullScreenElement() === this.wrapper) {
|
|
|
|
this.toggleFullScreen();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
protected toggleFullScreen() {
|
2020-08-27 20:25:47 +02:00
|
|
|
const player = this.wrapper;
|
2020-09-01 18:44:53 +02:00
|
|
|
|
|
|
|
// * https://caniuse.com/#feat=fullscreen
|
2021-09-26 15:59:10 +02:00
|
|
|
if(IS_APPLE_MOBILE) {
|
2020-09-01 18:44:53 +02:00
|
|
|
const video = this.video as any;
|
|
|
|
video.webkitEnterFullscreen();
|
|
|
|
video.enterFullscreen();
|
|
|
|
return;
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
if(!isFullScreen()) {
|
2020-08-27 20:25:47 +02:00
|
|
|
/* const videoParent = this.video.parentElement;
|
|
|
|
const videoWhichChild = whichChild(this.video);
|
2021-02-04 01:30:23 +01:00
|
|
|
const needVideoRemount = videoParent !== player;
|
2020-08-27 20:25:47 +02:00
|
|
|
|
|
|
|
if(needVideoRemount) {
|
|
|
|
this.videoParent = videoParent;
|
|
|
|
this.videoWhichChild = videoWhichChild;
|
|
|
|
player.prepend(this.video);
|
|
|
|
} */
|
2022-08-04 08:49:54 +02:00
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
requestFullScreen(player);
|
2024-05-18 19:55:12 +02:00
|
|
|
this.checkInteraction();
|
2020-04-25 03:17:50 +02:00
|
|
|
} else {
|
2020-08-27 20:25:47 +02:00
|
|
|
/* 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;
|
|
|
|
} */
|
2022-08-04 08:49:54 +02:00
|
|
|
|
2021-12-11 17:37:08 +01:00
|
|
|
cancelFullScreen();
|
|
|
|
}
|
|
|
|
}
|
2022-08-04 08:49:54 +02:00
|
|
|
|
2024-05-18 19:55:12 +02:00
|
|
|
public isFullScreen() {
|
|
|
|
return isFullScreen();
|
|
|
|
}
|
|
|
|
|
|
|
|
protected _onFullScreen(fullScreenButton: HTMLElement, noEvent?: boolean) {
|
2021-12-11 17:37:08 +01:00
|
|
|
const isFull = isFullScreen();
|
|
|
|
this.wrapper.classList.toggle('ckin__fullscreen', isFull);
|
|
|
|
if(!isFull) {
|
2023-09-06 22:28:19 +02:00
|
|
|
fullScreenButton.replaceChildren(Icon('fullscreen'));
|
2020-04-25 03:17:50 +02:00
|
|
|
fullScreenButton.setAttribute('title', 'Full Screen');
|
2021-12-11 17:37:08 +01:00
|
|
|
} else {
|
2023-09-06 22:28:19 +02:00
|
|
|
fullScreenButton.replaceChildren(Icon('smallscreen'));
|
2021-12-11 17:37:08 +01:00
|
|
|
fullScreenButton.setAttribute('title', 'Exit Full Screen');
|
2020-04-25 03:17:50 +02:00
|
|
|
}
|
2024-05-18 19:55:12 +02:00
|
|
|
|
|
|
|
!noEvent && this.onFullScreen?.(isFull);
|
2020-04-25 03:17:50 +02:00
|
|
|
}
|
2021-07-01 04:30:05 +02:00
|
|
|
|
2024-03-12 18:00:59 +01:00
|
|
|
public dimBackground() {
|
|
|
|
this.wrapper.classList.add('dim-background');
|
|
|
|
}
|
|
|
|
|
2023-03-03 16:47:02 +01:00
|
|
|
public setTimestamp(timestamp: number) {
|
2023-09-12 14:02:33 +02:00
|
|
|
setCurrentTime(this.video, timestamp);
|
2023-03-03 16:47:02 +01:00
|
|
|
this.togglePlay(true);
|
|
|
|
}
|
|
|
|
|
2022-04-15 20:04:56 +02:00
|
|
|
public cleanup() {
|
2021-08-11 15:39:49 +02:00
|
|
|
super.cleanup();
|
2021-07-01 04:30:05 +02:00
|
|
|
this.listenerSetter.removeAll();
|
2024-03-12 18:00:59 +01:00
|
|
|
this.progress?.removeListeners();
|
2024-05-18 19:55:12 +02:00
|
|
|
this.volumeSelector?.removeListeners();
|
|
|
|
this.onPlaybackRateMenuToggle =
|
|
|
|
this.onPip =
|
|
|
|
this.onVolumeChange =
|
|
|
|
this.onFullScreen =
|
|
|
|
this.onFullScreenToPip =
|
|
|
|
this.shouldEnableSoundOnClick =
|
|
|
|
undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
public unmount() {
|
|
|
|
[this.mainToggle, this.gradient, this.controls].forEach((element) => {
|
|
|
|
element.remove();
|
|
|
|
});
|
2021-07-01 04:30:05 +02:00
|
|
|
}
|
2024-03-12 18:00:59 +01:00
|
|
|
|
|
|
|
public setupLiveMenu(buttons: ButtonMenuItemOptionsVerifiable[]) {
|
|
|
|
this.liveMenuButton = ButtonMenuToggle({
|
|
|
|
direction: 'top-left',
|
|
|
|
buttons: buttons,
|
|
|
|
buttonOptions: {
|
|
|
|
noRipple: true
|
|
|
|
}
|
|
|
|
});
|
|
|
|
this.wrapper.querySelector('.right-controls').prepend(this.liveMenuButton);
|
|
|
|
}
|
|
|
|
|
|
|
|
public updateLiveViewersCount(count: number) {
|
|
|
|
this.wrapper.querySelector('.left-controls-watching').replaceChildren(i18n('Rtmp.Watching', [numberThousandSplitterForWatching(Math.max(1, count))]));
|
|
|
|
}
|
|
|
|
|
|
|
|
get inPip() {
|
|
|
|
return this._inPip;
|
|
|
|
}
|
2020-04-25 03:17:50 +02:00
|
|
|
}
|