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 = { @@ -55,12 +55,15 @@ type MediaDetails = {
isSingle?: boolean
};
export type PlaybackMediaType = 'voice' | 'video' | 'audio';
class AppMediaPlaybackController {
private container: HTMLElement;
private media: Map<PeerId, Map<number, HTMLMediaElement>> = new Map();
private scheduled: AppMediaPlaybackController['media'] = new Map();
private mediaDetails: Map<HTMLMediaElement, MediaDetails> = new Map();
private playingMedia: HTMLMediaElement;
private playingMediaType: PlaybackMediaType;
private waitingMediaForLoad: Map<PeerId, Map<number, CancellablePromise<void>>> = new Map();
private waitingScheduledMediaForLoad: AppMediaPlaybackController['waitingMediaForLoad'] = new Map();
@ -78,6 +81,11 @@ class AppMediaPlaybackController { @@ -78,6 +81,11 @@ class AppMediaPlaybackController {
private _muted = false;
private _playbackRate = 1;
private lockedSwitchers: boolean;
private playbackRates: Record<PlaybackMediaType, number> = {
voice: 1,
video: 1,
audio: 1
};
constructor() {
this.container = document.createElement('div');
@ -597,7 +605,10 @@ class AppMediaPlaybackController { @@ -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 { @@ -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;

18
src/components/appMediaViewerBase.ts

@ -1443,14 +1443,6 @@ export default class AppMediaViewerBase< @@ -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< @@ -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();

14
src/components/chat/audio.ts

@ -80,7 +80,13 @@ export default class ChatAudio extends PinnedContainer { @@ -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 { @@ -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 { @@ -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);

57
src/lib/mediaPlayer.ts

@ -263,11 +263,15 @@ export class VolumeSelector extends RangeSelector { @@ -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 { @@ -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 { @@ -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 { @@ -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<HTMLElement>;
const fullScreenButton = wrapper.querySelector('.fullscreen') as HTMLElement;
const timeElapsed = wrapper.querySelector('#time-elapsed');
@ -365,10 +371,14 @@ export default class VideoPlayer extends ControlsHover { @@ -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 { @@ -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 { @@ -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 { @@ -457,7 +471,7 @@ export default class VideoPlayer extends ControlsHover {
</div>
</div>
<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>
</div>
</div>
@ -466,19 +480,34 @@ export default class VideoPlayer extends ControlsHover { @@ -466,19 +480,34 @@ export default class VideoPlayer extends ControlsHover {
}
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 {
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() {

Loading…
Cancel
Save