Audio Player: Support 2X speed and mute; Audio: Fix width on mobile (#1533)

This commit is contained in:
Alexander Zinchuk 2021-11-05 21:57:51 +03:00
parent dedefe87ca
commit d6e1665e4e
11 changed files with 195 additions and 30 deletions

View File

@ -258,14 +258,14 @@ const Audio: FC<OwnProps> = ({
const isOwn = isOwnMessage(message);
const renderedWaveform = useMemo(
() => voice && renderWaveform(
() => origin === AudioOrigin.Inline && voice && renderWaveform(
voice,
(isMediaUnread && !isOwn) ? 1 : playProgress,
isOwn,
theme,
seekerRef,
),
[voice, isMediaUnread, isOwn, playProgress, theme],
[origin, voice, isMediaUnread, isOwn, playProgress, theme],
);
const fullClassName = buildClassName(

View File

@ -105,6 +105,22 @@
}
}
.playback-button {
&.applied {
--color-text-secondary: var(--color-primary);
}
.playback-button-inner {
transition: 0.2s color ease-in-out;
font-size: 0.75rem;
font-weight: bold;
border: 2px solid;
border-radius: 0.375rem;
padding: 0.15rem 0.25rem;
font-variant-numeric: tabular-nums;
}
}
&-content {
display: flex;
justify-content: center;

View File

@ -7,7 +7,8 @@ import {
ApiAudio, ApiChat, ApiMessage, ApiUser,
} from '../../api/types';
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/environment';
import * as mediaLoader from '../../util/mediaLoader';
import {
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle,
@ -39,9 +40,19 @@ type StateProps = {
sender?: ApiChat | ApiUser;
chat?: ApiChat;
volume: number;
playbackRate: number;
isMuted: boolean;
};
type DispatchProps = Pick<GlobalActions, 'focusMessage' | 'closeAudioPlayer' | 'setAudioPlayerVolume'>;
type DispatchProps = Pick<GlobalActions, (
'focusMessage' |
'closeAudioPlayer' |
'setAudioPlayerVolume' |
'setAudioPlayerPlaybackRate' |
'setAudioPlayerMuted'
)>;
const FAST_PLAYBACK_RATE = 1.8;
const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
message,
@ -51,19 +62,32 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
sender,
chat,
volume,
playbackRate,
isMuted,
setAudioPlayerVolume,
setAudioPlayerPlaybackRate,
setAudioPlayerMuted,
focusMessage,
closeAudioPlayer,
}) => {
const lang = useLang();
const { audio, voice } = getMessageContent(message);
const isVoice = Boolean(voice);
const { audio, voice, video } = getMessageContent(message);
const isVoice = Boolean(voice || video);
const senderName = sender ? getSenderTitle(lang, sender) : undefined;
const mediaData = mediaLoader.getFromMemory(getMessageMediaHash(message, 'inline')!) as (string | undefined);
const mediaMetadata = useMessageMediaMetadata(message, sender, chat);
const {
playPause, stop, isPlaying, requestNextTrack, requestPreviousTrack, isFirst, isLast, setVolume,
playPause,
stop,
isPlaying,
requestNextTrack,
requestPreviousTrack,
isFirst,
isLast,
setVolume,
toggleMuted,
setPlaybackRate,
} = useAudioPlayer(
makeTrackId(message),
getMediaDuration(message)!,
@ -94,15 +118,33 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
const handleVolumeChange = useCallback((value: number) => {
setAudioPlayerVolume({ volume: value / 100 });
setAudioPlayerMuted({ isMuted: false });
setVolume(value / 100);
}, [setAudioPlayerVolume, setVolume]);
}, [setAudioPlayerMuted, setAudioPlayerVolume, setVolume]);
const handleVolumeClick = useCallback(() => {
if (IS_TOUCH_ENV && !IS_IOS) return;
toggleMuted();
setAudioPlayerMuted({ isMuted: !isMuted });
}, [isMuted, setAudioPlayerMuted, toggleMuted]);
const handlePlaybackClick = useCallback(() => {
if (playbackRate === 1) {
setPlaybackRate(FAST_PLAYBACK_RATE);
setAudioPlayerPlaybackRate({ playbackRate: FAST_PLAYBACK_RATE });
} else {
setPlaybackRate(1);
setAudioPlayerPlaybackRate({ playbackRate: 1 });
}
}, [playbackRate, setAudioPlayerPlaybackRate, setPlaybackRate]);
const volumeIcon = useMemo(() => {
if (volume === 0) return 'icon-muted';
if (volume === 0 || isMuted) return 'icon-muted';
if (volume < 0.3) return 'icon-volume-1';
if (volume < 0.6) return 'icon-volume-2';
return 'icon-volume-3';
}, [volume]);
}, [volume, isMuted]);
if (noUi) {
return undefined;
@ -152,22 +194,39 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
<RippleEffect />
</div>
{!IS_IOS && (
<Button
round
className="player-button volume-button"
color="translucent"
size="smaller"
ariaLabel="Volume"
withClickPropagation
>
<i className={volumeIcon} onClick={handleVolumeClick} />
{!IS_IOS && (
<>
<div className="volume-slider-spacer" />
<div className="volume-slider">
<RangeSlider value={isMuted ? 0 : volume * 100} onChange={handleVolumeChange} />
</div>
</>
)}
</Button>
{isVoice && (
<Button
round
className="player-button volume-button"
className={buildClassName('playback-button', playbackRate !== 1 && 'applied')}
color="translucent"
size="smaller"
ariaLabel="Volume"
withClickPropagation
ariaLabel="Playback Rate"
ripple={!IS_SINGLE_COLUMN_LAYOUT}
onClick={handlePlaybackClick}
>
<i className={volumeIcon} />
<div className="volume-slider-spacer" />
<div className="volume-slider">
<RangeSlider value={volume * 100} onChange={handleVolumeChange} />
</div>
<span className="playback-button-inner">2Х</span>
</Button>
)}
<Button
round
className="player-close"
@ -208,13 +267,18 @@ export default withGlobal<OwnProps>(
(global, { message }): StateProps => {
const sender = selectSender(global, message);
const chat = selectChat(global, message.chatId);
const { volume } = global.audioPlayer;
const { volume, playbackRate, isMuted } = global.audioPlayer;
return {
sender,
chat,
volume,
playbackRate,
isMuted,
};
},
(setGlobal, actions): DispatchProps => pick(actions, ['focusMessage', 'closeAudioPlayer', 'setAudioPlayerVolume']),
(setGlobal, actions): DispatchProps => pick(
actions,
['focusMessage', 'closeAudioPlayer', 'setAudioPlayerVolume', 'setAudioPlayerPlaybackRate', 'setAudioPlayerMuted'],
),
)(AudioPlayer);

View File

@ -337,6 +337,10 @@
min-width: 20rem;
padding: .5rem .5rem .8125rem !important;
@media (max-width: 600px) {
min-width: 17rem;
}
.Audio + .text-content {
margin-top: .25rem;
}

View File

@ -61,6 +61,7 @@ export const TOP_CHAT_MESSAGES_PRELOAD_LIMIT = 20;
export const ALL_CHATS_PRELOAD_DISABLED = false;
export const DEFAULT_VOLUME = 1;
export const DEFAULT_PLAYBACK_RATE = 1;
export const ANIMATION_LEVEL_MIN = 0;
export const ANIMATION_LEVEL_MED = 1;

View File

@ -14,6 +14,7 @@ import {
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
GLOBAL_STATE_CACHE_USER_LIST_LIMIT,
DEFAULT_VOLUME,
DEFAULT_PLAYBACK_RATE,
} from '../config';
import { IS_SINGLE_COLUMN_LAYOUT } from '../util/environment';
import { ANIMATION_END_EVENT, ANIMATION_START_EVENT } from '../hooks/useHeavyAnimationCheck';
@ -137,6 +138,9 @@ function readCache(initialState: GlobalState): GlobalState {
if (cached.audioPlayer.volume === undefined) {
cached.audioPlayer.volume = DEFAULT_VOLUME;
}
if (cached.audioPlayer.playbackRate === undefined) {
cached.audioPlayer.playbackRate = DEFAULT_PLAYBACK_RATE;
}
}
const newState = {
@ -185,6 +189,8 @@ function updateCache() {
]),
audioPlayer: {
volume: global.audioPlayer.volume,
playbackRate: global.audioPlayer.playbackRate,
isMuted: global.audioPlayer.isMuted,
},
isChatInfoShown: reduceShowChatInfo(global),
users: reduceUsers(global),

View File

@ -3,6 +3,7 @@ import { NewChatMembersProgress } from '../types';
import {
ANIMATION_LEVEL_DEFAULT, DARK_THEME_PATTERN_COLOR, DEFAULT_MESSAGE_TEXT_SIZE_PX, DEFAULT_PATTERN_COLOR,
DEFAULT_PLAYBACK_RATE,
DEFAULT_VOLUME,
IOS_DEFAULT_MESSAGE_TEXT_SIZE_PX, MACOS_DEFAULT_MESSAGE_TEXT_SIZE_PX,
} from '../config';
@ -113,6 +114,8 @@ export const INITIAL_STATE: GlobalState = {
audioPlayer: {
volume: DEFAULT_VOLUME,
playbackRate: DEFAULT_PLAYBACK_RATE,
isMuted: false,
},
forwardMessages: {},

View File

@ -329,6 +329,8 @@ export type GlobalState = {
threadId?: number;
origin?: AudioOrigin;
volume: number;
playbackRate: number;
isMuted: boolean;
};
topPeers: {
@ -503,7 +505,7 @@ export type ActionTypes = (
'loadFullUser' | 'openUserInfo' | 'loadNearestCountry' | 'loadTopUsers' | 'loadContactList' |
'loadCurrentUser' | 'updateProfile' | 'checkUsername' | 'addContact' | 'updateContact' |
'deleteUser' | 'loadUser' | 'setUserSearchQuery' |
// Channel / groups creation
// chat creation
'createChannel' | 'createGroupChat' | 'resetChatCreation' |
// settings
'setSettingOption' | 'loadPasswordInfo' | 'clearTwoFaError' |
@ -514,7 +516,8 @@ export type ActionTypes = (
'updateWebNotificationSettings' | 'loadLanguages' | 'loadPrivacySettings' | 'setPrivacyVisibility' |
'setPrivacySettings' | 'loadNotificationExceptions' | 'setThemeSettings' | 'updateIsOnline' |
'loadContentSettings' | 'updateContentSettings' |
// Stickers & GIFs
'loadCountryList' | 'ensureTimeFormat' |
// stickers & GIFs
'loadStickerSets' | 'loadAddedStickers' | 'loadRecentStickers' | 'loadFavoriteStickers' | 'loadFeaturedStickers' |
'loadStickers' | 'setStickerSearchQuery' | 'loadSavedGifs' | 'setGifSearchQuery' | 'searchMoreGifs' |
'faveSticker' | 'unfaveSticker' | 'toggleStickerSet' | 'loadAnimatedEmojis' |
@ -524,12 +527,14 @@ export type ActionTypes = (
'clickInlineButton' | 'sendBotCommand' | 'loadTopInlineBots' | 'queryInlineBot' | 'sendInlineBotResult' |
'resetInlineBot' | 'restartBot' | 'startBot' |
// media viewer & audio player
'openMediaViewer' | 'closeMediaViewer' | 'openAudioPlayer' | 'setAudioPlayerVolume' | 'closeAudioPlayer' |
'openMediaViewer' | 'closeMediaViewer' |
'openAudioPlayer' | 'setAudioPlayerVolume' | 'setAudioPlayerPlaybackRate' |
'setAudioPlayerMuted' | 'closeAudioPlayer' |
// misc
'openPollModal' | 'closePollModal' | 'loadWebPagePreview' | 'clearWebPagePreview' |
'loadWallpapers' | 'uploadWallpaper' | 'setDeviceToken' | 'deleteDeviceToken' |
'openPollModal' | 'closePollModal' |
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' |
'setDeviceToken' | 'deleteDeviceToken' |
'checkVersionNotification' | 'createServiceNotification' |
'loadCountryList' | 'ensureTimeFormat' |
// payment
'openPaymentModal' | 'closePaymentModal' | 'addPaymentError' |
'validateRequestedInfo' | 'setPaymentStep' | 'sendPaymentForm' | 'getPaymentForm' | 'getReceipt' |

View File

@ -49,12 +49,18 @@ export default (
controllerRef.current = register(trackId, trackType, origin, (eventName, e) => {
switch (eventName) {
case 'onPlay': {
const { setVolume, proxy } = controllerRef.current!;
const {
setVolume, setPlaybackRate, toggleMuted, proxy,
} = controllerRef.current!;
setIsPlaying(true);
registerMediaSession(metadata, makeMediaHandlers(controllerRef));
setPlaybackState('playing');
setVolume(getGlobal().audioPlayer.volume);
toggleMuted(!!getGlobal().audioPlayer.isMuted);
if (trackType === 'voice') {
setPlaybackRate(getGlobal().audioPlayer.playbackRate);
}
setPositionState({
duration: proxy.duration || 0,
@ -63,6 +69,15 @@ export default (
});
break;
}
case 'onRateChange': {
const { proxy } = controllerRef.current!;
setPositionState({
duration: proxy.duration || 0,
playbackRate: proxy.playbackRate,
position: proxy.currentTime,
});
break;
}
case 'onPause':
setIsPlaying(false);
setPlaybackState('paused');
@ -111,6 +126,8 @@ export default (
isLast,
requestNextTrack,
requestPreviousTrack,
setPlaybackRate,
toggleMuted,
} = controllerRef.current!;
const duration = proxy.duration && Number.isFinite(proxy.duration) ? proxy.duration : originalDuration;
@ -179,6 +196,8 @@ export default (
requestPreviousTrack,
isFirst: isFirst(),
isLast: isLast(),
setPlaybackRate,
toggleMuted,
};
};

View File

@ -170,7 +170,7 @@ addReducer('closeMediaViewer', (global) => {
addReducer('openAudioPlayer', (global, actions, payload) => {
const {
chatId, threadId, messageId, origin, volume,
chatId, threadId, messageId, origin, volume, playbackRate, isMuted,
} = payload!;
return {
@ -181,6 +181,8 @@ addReducer('openAudioPlayer', (global, actions, payload) => {
messageId,
origin,
volume: volume || global.audioPlayer.volume,
playbackRate: playbackRate || global.audioPlayer.playbackRate,
isMuted: isMuted || global.audioPlayer.isMuted,
},
};
});
@ -199,11 +201,41 @@ addReducer('setAudioPlayerVolume', (global, actions, payload) => {
};
});
addReducer('setAudioPlayerPlaybackRate', (global, actions, payload) => {
const {
playbackRate,
} = payload!;
return {
...global,
audioPlayer: {
...global.audioPlayer,
playbackRate,
},
};
});
addReducer('setAudioPlayerMuted', (global, actions, payload) => {
const {
isMuted,
} = payload!;
return {
...global,
audioPlayer: {
...global.audioPlayer,
isMuted,
},
};
});
addReducer('closeAudioPlayer', (global) => {
return {
...global,
audioPlayer: {
volume: global.audioPlayer.volume, // Preserve only volume for the next play
volume: global.audioPlayer.volume,
playbackRate: global.audioPlayer.playbackRate,
isMuted: global.audioPlayer.isMuted,
},
};
});

View File

@ -211,6 +211,21 @@ export function register(
setVolume(volume: number) {
if (currentTrackId === trackId) {
audio.volume = volume;
audio.muted = false;
}
},
setPlaybackRate(rate: number) {
if (currentTrackId === trackId) {
audio.playbackRate = rate;
}
},
toggleMuted(muted?: boolean) {
if (muted === undefined) {
audio.muted = !audio.muted;
} else {
audio.muted = muted;
}
},