From d05488fc3122da05a653bbd681de10ae0f6fff76 Mon Sep 17 00:00:00 2001 From: Eduard Kuzmenko Date: Wed, 9 Feb 2022 17:34:34 +0400 Subject: [PATCH] Ability to change video and voice playback rate --- src/components/appMediaPlaybackController.ts | 30 +++++++++++ src/components/appMediaViewerBase.ts | 18 ++++--- src/components/chat/audio.ts | 14 ++++- src/lib/mediaPlayer.ts | 57 +++++++++++++++----- 4 files changed, 96 insertions(+), 23 deletions(-) diff --git a/src/components/appMediaPlaybackController.ts b/src/components/appMediaPlaybackController.ts index 03e68dab..1ad5997c 100644 --- a/src/components/appMediaPlaybackController.ts +++ b/src/components/appMediaPlaybackController.ts @@ -55,12 +55,15 @@ type MediaDetails = { isSingle?: boolean }; +export type PlaybackMediaType = 'voice' | 'video' | 'audio'; + class AppMediaPlaybackController { private container: HTMLElement; private media: Map> = new Map(); private scheduled: AppMediaPlaybackController['media'] = new Map(); private mediaDetails: Map = new Map(); private playingMedia: HTMLMediaElement; + private playingMediaType: PlaybackMediaType; private waitingMediaForLoad: Map>> = new Map(); private waitingScheduledMediaForLoad: AppMediaPlaybackController['waitingMediaForLoad'] = new Map(); @@ -78,6 +81,11 @@ class AppMediaPlaybackController { private _muted = false; private _playbackRate = 1; private lockedSwitchers: boolean; + private playbackRates: Record = { + voice: 1, + video: 1, + audio: 1 + }; constructor() { this.container = document.createElement('div'); @@ -597,7 +605,10 @@ class AppMediaPlaybackController { this.mediaDetails.delete(media); } + this.playbackRates[this.playingMediaType] = this.playbackRate; + this.playingMedia = undefined; + this.playingMediaType = undefined; return true; }; @@ -685,8 +696,27 @@ class AppMediaPlaybackController { this.listLoader.load(false); } + private getPlaybackMediaTypeFromMessage(message: Message.message) { + const doc = appMessagesManager.getMediaFromMessage(message) as MyDocument; + let mediaType: PlaybackMediaType = 'audio'; + if(doc?.type) { + if(doc.type === 'voice' || doc.type === 'round') { + mediaType = 'voice'; + } else if(doc.type === 'video') { + mediaType = 'video'; + } + } + + return mediaType; + } + public setMedia(media: HTMLMediaElement, message: Message.message) { + const mediaType = this.getPlaybackMediaTypeFromMessage(message); + + this._playbackRate = this.playbackRates[mediaType]; + this.playingMedia = media; + this.playingMediaType = mediaType; this.playingMedia.volume = this.volume; this.playingMedia.muted = this.muted; this.playingMedia.playbackRate = this.playbackRate; diff --git a/src/components/appMediaViewerBase.ts b/src/components/appMediaViewerBase.ts index 66410170..73419480 100644 --- a/src/components/appMediaViewerBase.ts +++ b/src/components/appMediaViewerBase.ts @@ -1443,14 +1443,6 @@ export default class AppMediaViewerBase< return; } - if(useController) { - const rollback = appMediaPlaybackController.setSingleMedia(video, message as Message.message); - - this.addEventListener('setMoverBefore', () => { - rollback(); - }, {once: true}); - } - const url = cacheContext.url; if(target instanceof SVGSVGElement/* && (video.parentElement || !isSafari) */) { // if video exists //if(!video.parentElement) { @@ -1460,6 +1452,16 @@ export default class AppMediaViewerBase< renderImageFromUrl(video, url); } + // * 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.addEventListener('setMoverBefore', () => { + rollback(); + }, {once: true}); + } + this.updateMediaSource(target, url, 'video'); createPlayer(); diff --git a/src/components/chat/audio.ts b/src/components/chat/audio.ts index bcb4ea39..b071da2b 100644 --- a/src/components/chat/audio.ts +++ b/src/components/chat/audio.ts @@ -80,7 +80,13 @@ export default class ChatAudio extends PinnedContainer { this.volumeSelector.btn.classList.add('pinned-audio-volume', 'active'); this.volumeSelector.btn.prepend(tunnel); this.volumeSelector.btn.append(volumeProgressLineContainer); - this.wrapperUtils.prepend(this.volumeSelector.btn); + + const fasterEl = ButtonIcon('playback_2x', {noRipple: true}); + attachClick(fasterEl, () => { + appMediaPlaybackController.playbackRate = fasterEl.classList.contains('active') ? 1 : 1.75; + }); + + this.wrapperUtils.prepend(this.volumeSelector.btn, fasterEl); const progressWrapper = document.createElement('div'); progressWrapper.classList.add('pinned-audio-progress-wrapper'); @@ -90,6 +96,10 @@ export default class ChatAudio extends PinnedContainer { progressWrapper.append(this.progressLine.container); this.wrapper.insertBefore(progressWrapper, this.wrapperUtils); + this.topbar.listenerSetter.add(rootScope)('media_playback_params', ({playbackRate}) => { + fasterEl.classList.toggle('active', playbackRate > 1); + }); + this.topbar.listenerSetter.add(rootScope)('media_play', ({doc, message, media}) => { let title: string | HTMLElement, subtitle: string | HTMLElement | DocumentFragment; if(doc.type === 'voice' || doc.type === 'round') { @@ -97,9 +107,11 @@ export default class ChatAudio extends PinnedContainer { //subtitle = 'Voice message'; subtitle = formatFullSentTime(message.date); + fasterEl.classList.remove('hide'); } else { title = doc.audioTitle || doc.fileName; subtitle = doc.audioPerformer || i18n('AudioUnknownArtist'); + fasterEl.classList.add('hide'); } this.progressLine.setMedia(media); diff --git a/src/lib/mediaPlayer.ts b/src/lib/mediaPlayer.ts index 483a480a..e9c9c09a 100644 --- a/src/lib/mediaPlayer.ts +++ b/src/lib/mediaPlayer.ts @@ -263,11 +263,15 @@ export class VolumeSelector extends RangeSelector { } export default class VideoPlayer extends ControlsHover { + private static PLAYBACK_RATES = [0.5, 1, 1.5, 2]; + private static PLAYBACK_RATES_ICONS = ['playback_05', 'playback_1x', 'playback_15', 'playback_2x']; + protected wrapper: HTMLDivElement; protected progress: MediaProgressLine; protected skin: 'default'; protected listenerSetter: ListenerSetter; + protected playbackRateButton: HTMLElement; /* protected videoParent: HTMLElement; protected videoWhichChild: number; */ @@ -284,7 +288,7 @@ export default class VideoPlayer extends ControlsHover { element: this.wrapper, listenerSetter: this.listenerSetter, canHideControls: () => { - return !this.video.paused; + return !this.video.paused && (!this.playbackRateButton || !this.playbackRateButton.classList.contains('menu-open')); }, showOnLeaveToClassName: 'media-viewer-caption' }); @@ -295,7 +299,7 @@ export default class VideoPlayer extends ControlsHover { this.skin = 'default'; this.stylePlayer(duration); - // this.setBtnMenuToggle(); + this.setBtnMenuToggle(); if(this.skin === 'default') { const controls = this.wrapper.querySelector('.default__controls.ckin__controls') as HTMLDivElement; @@ -328,6 +332,8 @@ export default class VideoPlayer extends ControlsHover { let timeDuration: HTMLElement; if(skin === 'default') { + this.playbackRateButton = this.wrapper.querySelector('.playback-rate') as HTMLElement; + const toggle = wrapper.querySelectorAll('.toggle') as NodeListOf; const fullScreenButton = wrapper.querySelector('.fullscreen') as HTMLElement; const timeElapsed = wrapper.querySelector('#time-elapsed'); @@ -365,10 +371,14 @@ export default class VideoPlayer extends ControlsHover { appMediaPlaybackController.muted = !appMediaPlaybackController.muted; } else if(code === 'Space') { this.togglePlay(); - } else if(e.altKey && code === 'Equal') { - appMediaPlaybackController.playbackRate += .25; - } else if(e.altKey && code === 'Minus') { - appMediaPlaybackController.playbackRate -= .25; + } else if(e.altKey && (code === 'Equal' || code === 'Minus')) { + const add = code === 'Equal' ? 1 : -1; + const playbackRate = appMediaPlaybackController.playbackRate; + const idx = VideoPlayer.PLAYBACK_RATES.indexOf(playbackRate); + const nextIdx = idx + add; + if(nextIdx >= 0 && nextIdx < VideoPlayer.PLAYBACK_RATES.length) { + appMediaPlaybackController.playbackRate = VideoPlayer.PLAYBACK_RATES[nextIdx]; + } } else if(wrapper.classList.contains('ckin__fullscreen') && (key === 'ArrowLeft' || key === 'ArrowRight')) { if(key === 'ArrowLeft') appMediaPlaybackController.seekBackward({action: 'seekbackward'}); else appMediaPlaybackController.seekForward({action: 'seekforward'}); @@ -417,6 +427,10 @@ export default class VideoPlayer extends ControlsHover { listenerSetter.add(video)('pause', () => { this.showControls(false); }); + + listenerSetter.add(rootScope)('media_playback_params', () => { + this.setPlaybackRateIcon(); + }); } listenerSetter.add(video)('play', () => { @@ -426,7 +440,7 @@ export default class VideoPlayer extends ControlsHover { listenerSetter.add(video)('pause', () => { wrapper.classList.remove('is-playing'); }); - + if(video.duration || initDuration) { timeDuration.innerHTML = String(Math.round(video.duration || initDuration)).toHHMMSS(); } else { @@ -457,7 +471,7 @@ export default class VideoPlayer extends ControlsHover {
- +
@@ -466,19 +480,34 @@ export default class VideoPlayer extends ControlsHover { } protected setBtnMenuToggle() { - const buttons: Parameters[0] = [0.25, 0.5, 1, 1.25, 1.5, 2].map((rate) => { + const buttons: Parameters[0] = VideoPlayer.PLAYBACK_RATES.map((rate, idx) => { return { - regularText: rate === 1 ? 'Normal' : '' + rate, + // icon: VideoPlayer.PLAYBACK_RATES_ICONS[idx], + regularText: rate + 'x', onClick: () => { - this.video.playbackRate = rate; + appMediaPlaybackController.playbackRate = rate; } }; }); const btnMenu = ButtonMenu(buttons); - const settingsButton = this.wrapper.querySelector('.settings') as HTMLElement; btnMenu.classList.add('top-left'); - ButtonMenuToggleHandler(settingsButton); - settingsButton.append(btnMenu); + ButtonMenuToggleHandler(this.playbackRateButton); + this.playbackRateButton.append(btnMenu); + + this.setPlaybackRateIcon(); + } + + protected setPlaybackRateIcon() { + const playbackRateButton = this.playbackRateButton; + VideoPlayer.PLAYBACK_RATES_ICONS.forEach((className) => { + className = 'tgico-' + className; + playbackRateButton.classList.remove(className); + }); + + let idx = VideoPlayer.PLAYBACK_RATES.indexOf(appMediaPlaybackController.playbackRate); + if(idx === -1) idx = VideoPlayer.PLAYBACK_RATES.indexOf(1); + + playbackRateButton.classList.add('tgico-' + VideoPlayer.PLAYBACK_RATES_ICONS[idx]); } protected toggleFullScreen() {