Browse Source

Ability to change video and voice playback rate

master
Eduard Kuzmenko 2 years ago
parent
commit
d05488fc31
  1. 30
      src/components/appMediaPlaybackController.ts
  2. 18
      src/components/appMediaViewerBase.ts
  3. 14
      src/components/chat/audio.ts
  4. 57
      src/lib/mediaPlayer.ts

30
src/components/appMediaPlaybackController.ts

@ -55,12 +55,15 @@ type MediaDetails = {
isSingle?: boolean isSingle?: boolean
}; };
export type PlaybackMediaType = 'voice' | 'video' | 'audio';
class AppMediaPlaybackController { class AppMediaPlaybackController {
private container: HTMLElement; private container: HTMLElement;
private media: Map<PeerId, Map<number, HTMLMediaElement>> = new Map(); private media: Map<PeerId, Map<number, HTMLMediaElement>> = new Map();
private scheduled: AppMediaPlaybackController['media'] = new Map(); private scheduled: AppMediaPlaybackController['media'] = new Map();
private mediaDetails: Map<HTMLMediaElement, MediaDetails> = new Map(); private mediaDetails: Map<HTMLMediaElement, MediaDetails> = new Map();
private playingMedia: HTMLMediaElement; private playingMedia: HTMLMediaElement;
private playingMediaType: PlaybackMediaType;
private waitingMediaForLoad: Map<PeerId, Map<number, CancellablePromise<void>>> = new Map(); private waitingMediaForLoad: Map<PeerId, Map<number, CancellablePromise<void>>> = new Map();
private waitingScheduledMediaForLoad: AppMediaPlaybackController['waitingMediaForLoad'] = new Map(); private waitingScheduledMediaForLoad: AppMediaPlaybackController['waitingMediaForLoad'] = new Map();
@ -78,6 +81,11 @@ class AppMediaPlaybackController {
private _muted = false; private _muted = false;
private _playbackRate = 1; private _playbackRate = 1;
private lockedSwitchers: boolean; private lockedSwitchers: boolean;
private playbackRates: Record<PlaybackMediaType, number> = {
voice: 1,
video: 1,
audio: 1
};
constructor() { constructor() {
this.container = document.createElement('div'); this.container = document.createElement('div');
@ -597,7 +605,10 @@ class AppMediaPlaybackController {
this.mediaDetails.delete(media); this.mediaDetails.delete(media);
} }
this.playbackRates[this.playingMediaType] = this.playbackRate;
this.playingMedia = undefined; this.playingMedia = undefined;
this.playingMediaType = undefined;
return true; return true;
}; };
@ -685,8 +696,27 @@ class AppMediaPlaybackController {
this.listLoader.load(false); 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) { public setMedia(media: HTMLMediaElement, message: Message.message) {
const mediaType = this.getPlaybackMediaTypeFromMessage(message);
this._playbackRate = this.playbackRates[mediaType];
this.playingMedia = media; this.playingMedia = media;
this.playingMediaType = mediaType;
this.playingMedia.volume = this.volume; this.playingMedia.volume = this.volume;
this.playingMedia.muted = this.muted; this.playingMedia.muted = this.muted;
this.playingMedia.playbackRate = this.playbackRate; this.playingMedia.playbackRate = this.playbackRate;

18
src/components/appMediaViewerBase.ts

@ -1443,14 +1443,6 @@ export default class AppMediaViewerBase<
return; return;
} }
if(useController) {
const rollback = appMediaPlaybackController.setSingleMedia(video, message as Message.message);
this.addEventListener('setMoverBefore', () => {
rollback();
}, {once: true});
}
const url = cacheContext.url; const url = cacheContext.url;
if(target instanceof SVGSVGElement/* && (video.parentElement || !isSafari) */) { // if video exists if(target instanceof SVGSVGElement/* && (video.parentElement || !isSafari) */) { // if video exists
//if(!video.parentElement) { //if(!video.parentElement) {
@ -1460,6 +1452,16 @@ export default class AppMediaViewerBase<
renderImageFromUrl(video, url); 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'); this.updateMediaSource(target, url, 'video');
createPlayer(); createPlayer();

14
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.classList.add('pinned-audio-volume', 'active');
this.volumeSelector.btn.prepend(tunnel); this.volumeSelector.btn.prepend(tunnel);
this.volumeSelector.btn.append(volumeProgressLineContainer); 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'); const progressWrapper = document.createElement('div');
progressWrapper.classList.add('pinned-audio-progress-wrapper'); progressWrapper.classList.add('pinned-audio-progress-wrapper');
@ -90,6 +96,10 @@ export default class ChatAudio extends PinnedContainer {
progressWrapper.append(this.progressLine.container); progressWrapper.append(this.progressLine.container);
this.wrapper.insertBefore(progressWrapper, this.wrapperUtils); 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}) => { this.topbar.listenerSetter.add(rootScope)('media_play', ({doc, message, media}) => {
let title: string | HTMLElement, subtitle: string | HTMLElement | DocumentFragment; let title: string | HTMLElement, subtitle: string | HTMLElement | DocumentFragment;
if(doc.type === 'voice' || doc.type === 'round') { if(doc.type === 'voice' || doc.type === 'round') {
@ -97,9 +107,11 @@ export default class ChatAudio extends PinnedContainer {
//subtitle = 'Voice message'; //subtitle = 'Voice message';
subtitle = formatFullSentTime(message.date); subtitle = formatFullSentTime(message.date);
fasterEl.classList.remove('hide');
} else { } else {
title = doc.audioTitle || doc.fileName; title = doc.audioTitle || doc.fileName;
subtitle = doc.audioPerformer || i18n('AudioUnknownArtist'); subtitle = doc.audioPerformer || i18n('AudioUnknownArtist');
fasterEl.classList.add('hide');
} }
this.progressLine.setMedia(media); this.progressLine.setMedia(media);

57
src/lib/mediaPlayer.ts

@ -263,11 +263,15 @@ export class VolumeSelector extends RangeSelector {
} }
export default class VideoPlayer extends ControlsHover { 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 wrapper: HTMLDivElement;
protected progress: MediaProgressLine; protected progress: MediaProgressLine;
protected skin: 'default'; protected skin: 'default';
protected listenerSetter: ListenerSetter; protected listenerSetter: ListenerSetter;
protected playbackRateButton: HTMLElement;
/* protected videoParent: HTMLElement; /* protected videoParent: HTMLElement;
protected videoWhichChild: number; */ protected videoWhichChild: number; */
@ -284,7 +288,7 @@ export default class VideoPlayer extends ControlsHover {
element: this.wrapper, element: this.wrapper,
listenerSetter: this.listenerSetter, listenerSetter: this.listenerSetter,
canHideControls: () => { canHideControls: () => {
return !this.video.paused; return !this.video.paused && (!this.playbackRateButton || !this.playbackRateButton.classList.contains('menu-open'));
}, },
showOnLeaveToClassName: 'media-viewer-caption' showOnLeaveToClassName: 'media-viewer-caption'
}); });
@ -295,7 +299,7 @@ export default class VideoPlayer extends ControlsHover {
this.skin = 'default'; this.skin = 'default';
this.stylePlayer(duration); this.stylePlayer(duration);
// this.setBtnMenuToggle(); this.setBtnMenuToggle();
if(this.skin === 'default') { if(this.skin === 'default') {
const controls = this.wrapper.querySelector('.default__controls.ckin__controls') as HTMLDivElement; const controls = this.wrapper.querySelector('.default__controls.ckin__controls') as HTMLDivElement;
@ -328,6 +332,8 @@ export default class VideoPlayer extends ControlsHover {
let timeDuration: HTMLElement; let timeDuration: HTMLElement;
if(skin === 'default') { if(skin === 'default') {
this.playbackRateButton = this.wrapper.querySelector('.playback-rate') as HTMLElement;
const toggle = wrapper.querySelectorAll('.toggle') as NodeListOf<HTMLElement>; const toggle = wrapper.querySelectorAll('.toggle') as NodeListOf<HTMLElement>;
const fullScreenButton = wrapper.querySelector('.fullscreen') as HTMLElement; const fullScreenButton = wrapper.querySelector('.fullscreen') as HTMLElement;
const timeElapsed = wrapper.querySelector('#time-elapsed'); const timeElapsed = wrapper.querySelector('#time-elapsed');
@ -365,10 +371,14 @@ export default class VideoPlayer extends ControlsHover {
appMediaPlaybackController.muted = !appMediaPlaybackController.muted; appMediaPlaybackController.muted = !appMediaPlaybackController.muted;
} else if(code === 'Space') { } else if(code === 'Space') {
this.togglePlay(); this.togglePlay();
} else if(e.altKey && code === 'Equal') { } else if(e.altKey && (code === 'Equal' || code === 'Minus')) {
appMediaPlaybackController.playbackRate += .25; const add = code === 'Equal' ? 1 : -1;
} else if(e.altKey && code === 'Minus') { const playbackRate = appMediaPlaybackController.playbackRate;
appMediaPlaybackController.playbackRate -= .25; 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')) { } else if(wrapper.classList.contains('ckin__fullscreen') && (key === 'ArrowLeft' || key === 'ArrowRight')) {
if(key === 'ArrowLeft') appMediaPlaybackController.seekBackward({action: 'seekbackward'}); if(key === 'ArrowLeft') appMediaPlaybackController.seekBackward({action: 'seekbackward'});
else appMediaPlaybackController.seekForward({action: 'seekforward'}); else appMediaPlaybackController.seekForward({action: 'seekforward'});
@ -417,6 +427,10 @@ export default class VideoPlayer extends ControlsHover {
listenerSetter.add(video)('pause', () => { listenerSetter.add(video)('pause', () => {
this.showControls(false); this.showControls(false);
}); });
listenerSetter.add(rootScope)('media_playback_params', () => {
this.setPlaybackRateIcon();
});
} }
listenerSetter.add(video)('play', () => { listenerSetter.add(video)('play', () => {
@ -426,7 +440,7 @@ export default class VideoPlayer extends ControlsHover {
listenerSetter.add(video)('pause', () => { listenerSetter.add(video)('pause', () => {
wrapper.classList.remove('is-playing'); wrapper.classList.remove('is-playing');
}); });
if(video.duration || initDuration) { if(video.duration || initDuration) {
timeDuration.innerHTML = String(Math.round(video.duration || initDuration)).toHHMMSS(); timeDuration.innerHTML = String(Math.round(video.duration || initDuration)).toHHMMSS();
} else { } else {
@ -457,7 +471,7 @@ export default class VideoPlayer extends ControlsHover {
</div> </div>
</div> </div>
<div class="right-controls"> <div class="right-controls">
<button class="btn-icon ${skin}__button btn-menu-toggle settings tgico-settings hide" title="Playback Rate"></button> <button class="btn-icon ${skin}__button btn-menu-toggle playback-rate night" title="Playback Rate"></button>
<button class="btn-icon ${skin}__button fullscreen tgico-fullscreen" title="Full Screen"></button> <button class="btn-icon ${skin}__button fullscreen tgico-fullscreen" title="Full Screen"></button>
</div> </div>
</div> </div>
@ -466,19 +480,34 @@ export default class VideoPlayer extends ControlsHover {
} }
protected setBtnMenuToggle() { protected setBtnMenuToggle() {
const buttons: Parameters<typeof ButtonMenu>[0] = [0.25, 0.5, 1, 1.25, 1.5, 2].map((rate) => { const buttons: Parameters<typeof ButtonMenu>[0] = VideoPlayer.PLAYBACK_RATES.map((rate, idx) => {
return { return {
regularText: rate === 1 ? 'Normal' : '' + rate, // icon: VideoPlayer.PLAYBACK_RATES_ICONS[idx],
regularText: rate + 'x',
onClick: () => { onClick: () => {
this.video.playbackRate = rate; appMediaPlaybackController.playbackRate = rate;
} }
}; };
}); });
const btnMenu = ButtonMenu(buttons); const btnMenu = ButtonMenu(buttons);
const settingsButton = this.wrapper.querySelector('.settings') as HTMLElement;
btnMenu.classList.add('top-left'); btnMenu.classList.add('top-left');
ButtonMenuToggleHandler(settingsButton); ButtonMenuToggleHandler(this.playbackRateButton);
settingsButton.append(btnMenu); 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() { protected toggleFullScreen() {

Loading…
Cancel
Save