Audio Player: Fix queue order, memory leaks and reduce calculations (#1543)

This commit is contained in:
Alexander Zinchuk 2021-11-27 17:40:56 +01:00
parent 1a6f0bb28f
commit 50865c78f5
10 changed files with 77 additions and 70 deletions

View File

@ -125,7 +125,6 @@ const Audio: FC<OwnProps> = ({
makeTrackId(message), makeTrackId(message),
getMediaDuration(message)!, getMediaDuration(message)!,
isVoice ? 'voice' : 'audio', isVoice ? 'voice' : 'audio',
origin,
mediaData, mediaData,
bufferingHandlers, bufferingHandlers,
undefined, undefined,
@ -172,9 +171,10 @@ const Audio: FC<OwnProps> = ({
onPlay(message.id, message.chatId); onPlay(message.id, message.chatId);
} }
getDispatch().setAudioPlayerOrigin({ origin });
setIsActivated(!isActivated); setIsActivated(!isActivated);
playPause(); playPause();
}, [isPlaying, isUploading, message.id, message.chatId, onCancelUpload, onPlay, playPause, isActivated]); }, [isUploading, isPlaying, isActivated, playPause, onCancelUpload, onPlay, message.id, message.chatId, origin]);
useEffect(() => { useEffect(() => {
if (onReadMedia && isMediaUnread && (isPlaying || isDownloading)) { if (onReadMedia && isMediaUnread && (isPlaying || isDownloading)) {

View File

@ -77,7 +77,7 @@ const AudioResults: FC<OwnProps & StateProps & DispatchProps> = ({
}, [focusMessage]); }, [focusMessage]);
const handlePlayAudio = useCallback((messageId: number, chatId: string) => { const handlePlayAudio = useCallback((messageId: number, chatId: string) => {
openAudioPlayer({ chatId, messageId, origin: AudioOrigin.Search }); openAudioPlayer({ chatId, messageId });
}, [openAudioPlayer]); }, [openAudioPlayer]);
function renderList() { function renderList() {

View File

@ -3,7 +3,7 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getGlobal, withGlobal } from '../../lib/teact/teactn'; import { getGlobal, withGlobal } from '../../lib/teact/teactn';
import { AudioOrigin, LangCode } from '../../types'; import { LangCode } from '../../types';
import { GlobalActions } from '../../global/types'; import { GlobalActions } from '../../global/types';
import { ApiMessage } from '../../api/types'; import { ApiMessage } from '../../api/types';
@ -56,7 +56,6 @@ type StateProps = {
hasNotifications: boolean; hasNotifications: boolean;
hasDialogs: boolean; hasDialogs: boolean;
audioMessage?: ApiMessage; audioMessage?: ApiMessage;
audioOrigin?: AudioOrigin;
safeLinkModalUrl?: string; safeLinkModalUrl?: string;
isHistoryCalendarOpen: boolean; isHistoryCalendarOpen: boolean;
shouldSkipHistoryAnimations?: boolean; shouldSkipHistoryAnimations?: boolean;
@ -89,7 +88,6 @@ const Main: FC<StateProps & DispatchProps> = ({
hasNotifications, hasNotifications,
hasDialogs, hasDialogs,
audioMessage, audioMessage,
audioOrigin,
safeLinkModalUrl, safeLinkModalUrl,
isHistoryCalendarOpen, isHistoryCalendarOpen,
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
@ -265,7 +263,7 @@ const Main: FC<StateProps & DispatchProps> = ({
<ForwardPicker isOpen={isForwardModalOpen} /> <ForwardPicker isOpen={isForwardModalOpen} />
<Notifications isOpen={hasNotifications} /> <Notifications isOpen={hasNotifications} />
<Dialogs isOpen={hasDialogs} /> <Dialogs isOpen={hasDialogs} />
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} origin={audioOrigin} noUi />} {audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
<SafeLinkModal url={safeLinkModalUrl} /> <SafeLinkModal url={safeLinkModalUrl} />
<HistoryCalendar isOpen={isHistoryCalendarOpen} /> <HistoryCalendar isOpen={isHistoryCalendarOpen} />
<StickerSetModal <StickerSetModal
@ -302,7 +300,7 @@ function updatePageTitle(nextTitle: string) {
export default memo(withGlobal( export default memo(withGlobal(
(global): StateProps => { (global): StateProps => {
const { settings: { byKey: { animationLevel, language, wasTimeFormatSetManually } } } = global; const { settings: { byKey: { animationLevel, language, wasTimeFormatSetManually } } } = global;
const { chatId: audioChatId, messageId: audioMessageId, origin } = global.audioPlayer; const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer;
const audioMessage = audioChatId && audioMessageId const audioMessage = audioChatId && audioMessageId
? selectChatMessage(global, audioChatId, audioMessageId) ? selectChatMessage(global, audioChatId, audioMessageId)
: undefined; : undefined;
@ -316,7 +314,6 @@ export default memo(withGlobal(
hasNotifications: Boolean(global.notifications.length), hasNotifications: Boolean(global.notifications.length),
hasDialogs: Boolean(global.dialogs.length), hasDialogs: Boolean(global.dialogs.length),
audioMessage, audioMessage,
audioOrigin: origin,
safeLinkModalUrl: global.safeLinkModalUrl, safeLinkModalUrl: global.safeLinkModalUrl,
isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt), isHistoryCalendarOpen: Boolean(global.historyCalendarSelectedAt),
shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations, shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations,

View File

@ -13,7 +13,7 @@ import { IS_IOS, IS_SINGLE_COLUMN_LAYOUT, IS_TOUCH_ENV } from '../../util/enviro
import * as mediaLoader from '../../util/mediaLoader'; import * as mediaLoader from '../../util/mediaLoader';
import { import {
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle, getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle, isMessageLocal,
} from '../../modules/helpers'; } from '../../modules/helpers';
import { selectChat, selectSender } from '../../modules/selectors'; import { selectChat, selectSender } from '../../modules/selectors';
import { pick } from '../../util/iteratees'; import { pick } from '../../util/iteratees';
@ -59,7 +59,6 @@ const FAST_PLAYBACK_RATE = 1.8;
const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
message, message,
origin = AudioOrigin.Inline,
className, className,
noUi, noUi,
sender, sender,
@ -95,7 +94,6 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
makeTrackId(message), makeTrackId(message),
getMediaDuration(message)!, getMediaDuration(message)!,
isVoice ? 'voice' : 'audio', isVoice ? 'voice' : 'audio',
origin,
mediaData, mediaData,
undefined, undefined,
mediaMetadata, mediaMetadata,
@ -103,7 +101,7 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
true, true,
undefined, undefined,
undefined, undefined,
true, isMessageLocal(message),
true, true,
); );
@ -176,7 +174,7 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
color="translucent" color="translucent"
size="smaller" size="smaller"
className="player-button" className="player-button"
disabled={isFirst} disabled={isFirst()}
onClick={requestPreviousTrack} onClick={requestPreviousTrack}
ariaLabel="Previous track" ariaLabel="Previous track"
> >
@ -200,7 +198,7 @@ const AudioPlayer: FC<OwnProps & StateProps & DispatchProps> = ({
color="translucent" color="translucent"
size="smaller" size="smaller"
className="player-button" className="player-button"
disabled={isLast} disabled={isLast()}
onClick={requestNextTrack} onClick={requestNextTrack}
ariaLabel="Next track" ariaLabel="Next track"
> >

View File

@ -2,7 +2,7 @@ import React, { useCallback } from '../../../../lib/teact/teact';
import { getDispatch } from '../../../../lib/teact/teactn'; import { getDispatch } from '../../../../lib/teact/teactn';
import { isUserId } from '../../../../modules/helpers'; import { isUserId } from '../../../../modules/helpers';
import { AudioOrigin, IAlbum, MediaViewerOrigin } from '../../../../types'; import { IAlbum, MediaViewerOrigin } from '../../../../types';
import { import {
ApiChat, ApiMessage, ApiUser, MAIN_THREAD_ID, ApiChat, ApiMessage, ApiUser, MAIN_THREAD_ID,
} from '../../../../api/types'; } from '../../../../api/types';
@ -81,7 +81,7 @@ export default function useInnerHandlers(
}, [chatId, threadId, messageId, openMediaViewer, isScheduled]); }, [chatId, threadId, messageId, openMediaViewer, isScheduled]);
const handleAudioPlay = useCallback((): void => { const handleAudioPlay = useCallback((): void => {
openAudioPlayer({ chatId, messageId, origin: AudioOrigin.Inline }); openAudioPlayer({ chatId, messageId });
}, [chatId, messageId, openAudioPlayer]); }, [chatId, messageId, openAudioPlayer]);
const handleAlbumMediaClick = useCallback((albumMessageId: number): void => { const handleAlbumMediaClick = useCallback((albumMessageId: number): void => {

View File

@ -210,7 +210,7 @@ const Profile: FC<OwnProps & StateProps & DispatchProps> = ({
}, [profileId, openMediaViewer]); }, [profileId, openMediaViewer]);
const handlePlayAudio = useCallback((messageId: number) => { const handlePlayAudio = useCallback((messageId: number) => {
openAudioPlayer({ chatId: profileId, messageId, origin: AudioOrigin.SharedMedia }); openAudioPlayer({ chatId: profileId, messageId });
}, [profileId, openAudioPlayer]); }, [profileId, openAudioPlayer]);
const handleMemberClick = useCallback((id: string) => { const handleMemberClick = useCallback((id: string) => {

View File

@ -530,7 +530,7 @@ export type ActionTypes = (
// media viewer & audio player // media viewer & audio player
'openMediaViewer' | 'closeMediaViewer' | 'openMediaViewer' | 'closeMediaViewer' |
'openAudioPlayer' | 'setAudioPlayerVolume' | 'setAudioPlayerPlaybackRate' | 'openAudioPlayer' | 'setAudioPlayerVolume' | 'setAudioPlayerPlaybackRate' |
'setAudioPlayerMuted' | 'closeAudioPlayer' | 'setAudioPlayerMuted' | 'setAudioPlayerOrigin' | 'closeAudioPlayer' |
// misc // misc
'openPollModal' | 'closePollModal' | 'openPollModal' | 'closePollModal' |
'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' | 'loadWebPagePreview' | 'clearWebPagePreview' | 'loadWallpapers' | 'uploadWallpaper' |

View File

@ -3,8 +3,6 @@ import {
} from '../lib/teact/teact'; } from '../lib/teact/teact';
import { getDispatch, getGlobal } from '../lib/teact/teactn'; import { getDispatch, getGlobal } from '../lib/teact/teactn';
import { AudioOrigin } from '../types';
import { register, Track, TrackId } from '../util/audioPlayer'; import { register, Track, TrackId } from '../util/audioPlayer';
import useEffectWithPrevDeps from './useEffectWithPrevDeps'; import useEffectWithPrevDeps from './useEffectWithPrevDeps';
import { isSafariPatchInProgress } from '../util/patchSafariProgressiveAudio'; import { isSafariPatchInProgress } from '../util/patchSafariProgressiveAudio';
@ -21,7 +19,6 @@ export default (
trackId: TrackId, trackId: TrackId,
originalDuration: number, // Sometimes incorrect for voice messages originalDuration: number, // Sometimes incorrect for voice messages
trackType: Track['type'], trackType: Track['type'],
origin: AudioOrigin,
src?: string, src?: string,
handlers?: Record<string, Handler>, handlers?: Record<string, Handler>,
metadata?: MediaMetadata, metadata?: MediaMetadata,
@ -46,7 +43,7 @@ export default (
}, [onTrackChange]); }, [onTrackChange]);
useOnChange(() => { useOnChange(() => {
controllerRef.current = register(trackId, trackType, origin, (eventName, e) => { controllerRef.current = register(trackId, trackType, (eventName, e) => {
switch (eventName) { switch (eventName) {
case 'onPlay': { case 'onPlay': {
const { const {
@ -120,7 +117,6 @@ export default (
proxy, proxy,
destroy, destroy,
setVolume, setVolume,
setCurrentOrigin,
stop, stop,
isFirst, isFirst,
isLast, isLast,
@ -161,10 +157,9 @@ export default (
const playIfPresent = useCallback(() => { const playIfPresent = useCallback(() => {
if (src) { if (src) {
setCurrentOrigin(origin);
play(src); play(src);
} }
}, [src, origin, play, setCurrentOrigin]); }, [src, play]);
const playPause = useCallback(() => { const playPause = useCallback(() => {
if (isPlaying) { if (isPlaying) {
@ -194,8 +189,8 @@ export default (
duration, duration,
requestNextTrack, requestNextTrack,
requestPreviousTrack, requestPreviousTrack,
isFirst: isFirst(), isFirst,
isLast: isLast(), isLast,
setPlaybackRate, setPlaybackRate,
toggleMuted, toggleMuted,
}; };

View File

@ -179,8 +179,8 @@ addReducer('openAudioPlayer', (global, actions, payload) => {
chatId, chatId,
threadId, threadId,
messageId, messageId,
origin, origin: origin ?? global.audioPlayer.origin,
volume: volume || global.audioPlayer.volume, volume: volume ?? global.audioPlayer.volume,
playbackRate: playbackRate || global.audioPlayer.playbackRate, playbackRate: playbackRate || global.audioPlayer.playbackRate,
isMuted: isMuted || global.audioPlayer.isMuted, isMuted: isMuted || global.audioPlayer.isMuted,
}, },
@ -229,6 +229,20 @@ addReducer('setAudioPlayerMuted', (global, actions, payload) => {
}; };
}); });
addReducer('setAudioPlayerOrigin', (global, actions, payload) => {
const {
origin,
} = payload!;
return {
...global,
audioPlayer: {
...global.audioPlayer,
origin,
},
};
});
addReducer('closeAudioPlayer', (global) => { addReducer('closeAudioPlayer', (global) => {
return { return {
...global, ...global,

View File

@ -1,6 +1,6 @@
import { getDispatch } from '../lib/teact/teactn'; import { getDispatch, getGlobal } from '../lib/teact/teactn';
import { AudioOrigin } from '../types'; import { AudioOrigin, GlobalSearchContent } from '../types';
import { ApiMessage } from '../api/types'; import { ApiMessage } from '../api/types';
import { IS_SAFARI } from './environment'; import { IS_SAFARI } from './environment';
@ -8,6 +8,7 @@ import safePlay from './safePlay';
import { patchSafariProgressiveAudio, isSafariPatchInProgress } from './patchSafariProgressiveAudio'; import { patchSafariProgressiveAudio, isSafariPatchInProgress } from './patchSafariProgressiveAudio';
import { getMessageKey, MessageKey, parseMessageKey } from '../modules/helpers'; import { getMessageKey, MessageKey, parseMessageKey } from '../modules/helpers';
import { fastRaf } from './schedulers'; import { fastRaf } from './schedulers';
import { selectCurrentMessageList } from '../modules/selectors';
type Handler = (eventName: string, e: Event) => void; type Handler = (eventName: string, e: Event) => void;
export type TrackId = `${MessageKey}-${number}`; export type TrackId = `${MessageKey}-${number}`;
@ -16,22 +17,20 @@ export interface Track {
audio: HTMLAudioElement; audio: HTMLAudioElement;
proxy: HTMLAudioElement; proxy: HTMLAudioElement;
type: 'voice' | 'audio'; type: 'voice' | 'audio';
origin: AudioOrigin;
handlers: Handler[]; handlers: Handler[];
onForcePlay?: NoneToVoidFunction; onForcePlay?: NoneToVoidFunction;
onTrackChange?: NoneToVoidFunction; onTrackChange?: NoneToVoidFunction;
} }
const tracks = new Map<string, Track>(); const tracks = new Map<TrackId, Track>();
let voiceQueue: TrackId[] = []; let voiceQueue: TrackId[] = [];
let musicQueue: TrackId[] = []; let musicQueue: TrackId[] = [];
let currentTrackId: string | undefined; let currentTrackId: TrackId | undefined;
function createAudio( function createAudio(
trackId: TrackId, trackId: TrackId,
type: Track['type'], type: Track['type'],
origin: AudioOrigin,
onForcePlay?: NoneToVoidFunction, onForcePlay?: NoneToVoidFunction,
onTrackChange?: NoneToVoidFunction, onTrackChange?: NoneToVoidFunction,
): Track { ): Track {
@ -77,7 +76,6 @@ function createAudio(
proxy: new Proxy(audio, { proxy: new Proxy(audio, {
get: (target, key: keyof HTMLAudioElement) => target[key], get: (target, key: keyof HTMLAudioElement) => target[key],
}), }),
origin,
handlers: [], handlers: [],
onForcePlay, onForcePlay,
onTrackChange, onTrackChange,
@ -92,11 +90,9 @@ function playNext(trackId: TrackId, isReverseOrder?: boolean) {
if (currentTrack.onTrackChange) currentTrack.onTrackChange(); if (currentTrack.onTrackChange) currentTrack.onTrackChange();
} }
const track = tracks.get(trackId)!; const origin = getGlobal().audioPlayer.origin || AudioOrigin.Inline;
const queue = getTrackQueue(track);
if (!queue) return;
const nextTrackId = findNextInQueue(queue, trackId, track.origin, isReverseOrder); const nextTrackId = findNextInQueue(trackId, origin, isReverseOrder);
if (!nextTrackId) { if (!nextTrackId) {
return; return;
} }
@ -110,8 +106,6 @@ function playNext(trackId: TrackId, isReverseOrder?: boolean) {
const nextTrack = tracks.get(nextTrackId)!; const nextTrack = tracks.get(nextTrackId)!;
if (currentTrack) nextTrack.origin = currentTrack.origin; // Preserve origin
if (nextTrack.onForcePlay) { if (nextTrack.onForcePlay) {
nextTrack.onForcePlay(); nextTrack.onForcePlay();
} }
@ -133,13 +127,12 @@ export function stopCurrentAudio() {
export function register( export function register(
trackId: TrackId, trackId: TrackId,
trackType: Track['type'], trackType: Track['type'],
origin: AudioOrigin,
handler: Handler, handler: Handler,
onForcePlay?: NoneToVoidFunction, onForcePlay?: NoneToVoidFunction,
onTrackChange?: NoneToVoidFunction, onTrackChange?: NoneToVoidFunction,
) { ) {
if (!tracks.has(trackId)) { if (!tracks.has(trackId)) {
const track = createAudio(trackId, trackType, origin, onForcePlay, onTrackChange); const track = createAudio(trackId, trackType, onForcePlay, onTrackChange);
tracks.set(trackId, track); tracks.set(trackId, track);
addTrackToQueue(track, trackId); addTrackToQueue(track, trackId);
} }
@ -173,10 +166,7 @@ export function register(
} }
safePlay(audio); safePlay(audio);
}, cleanUpQueue(trackType, trackId);
setCurrentOrigin(audioOrigin: AudioOrigin) {
tracks.get(trackId)!.origin = audioOrigin;
}, },
pause() { pause() {
@ -236,17 +226,11 @@ export function register(
}, },
isLast() { isLast() {
const track = tracks.get(trackId)!; return !findNextInQueue(trackId, getGlobal().audioPlayer.origin);
const queue = getTrackQueue(track);
if (!queue) return true;
return !findNextInQueue(queue, trackId, tracks.get(trackId)!.origin);
}, },
isFirst() { isFirst() {
const track = tracks.get(trackId)!; return !findNextInQueue(trackId, getGlobal().audioPlayer.origin, true);
const queue = getTrackQueue(track);
if (!queue) return true;
return !findNextInQueue(queue, trackId, tracks.get(trackId)!.origin, true);
}, },
requestPreviousTrack() { requestPreviousTrack() {
@ -278,14 +262,11 @@ export function register(
} }
function getTrackQueue(track: Track) { function getTrackQueue(track: Track) {
if (track.type === 'audio') { switch (track.type) {
return musicQueue; case 'audio': return musicQueue;
case 'voice': return voiceQueue;
default: return undefined;
} }
if (track.type === 'voice') {
return voiceQueue;
}
return undefined;
} }
function addTrackToQueue(track: Track, trackId: TrackId) { function addTrackToQueue(track: Track, trackId: TrackId) {
@ -301,26 +282,48 @@ function addTrackToQueue(track: Track, trackId: TrackId) {
} }
function removeFromQueue(track: Track, trackId: TrackId) { function removeFromQueue(track: Track, trackId: TrackId) {
const trackIdFilter = (el: TrackId) => el !== trackId;
if (track.type === 'audio') { if (track.type === 'audio') {
musicQueue = musicQueue.filter((el) => el !== trackId); musicQueue = musicQueue.filter(trackIdFilter);
} }
if (track.type === 'voice') { if (track.type === 'voice') {
voiceQueue = voiceQueue.filter((el) => el !== trackId); voiceQueue = voiceQueue.filter(trackIdFilter);
} }
} }
function findNextInQueue(queue: TrackId[], current: TrackId, origin: AudioOrigin, isReverseOrder?: boolean) { function cleanUpQueue(type: Track['type'], trackId: TrackId) {
if (getGlobal().globalSearch.currentContent === GlobalSearchContent.Music) return;
const { chatId } = parseMessageKey(splitTrackId(trackId).messageKey);
const openedChatId = selectCurrentMessageList(getGlobal())?.chatId;
const queueFilter = (id: string) => (
id.startsWith(`msg${chatId}`) || (openedChatId && id.startsWith(`msg${openedChatId}`))
);
if (type === 'audio') {
musicQueue = musicQueue.filter(queueFilter);
}
if (type === 'voice') {
voiceQueue = voiceQueue.filter(queueFilter);
}
}
function findNextInQueue(currentId: TrackId, origin = AudioOrigin.Inline, isReverseOrder?: boolean) {
const track = tracks.get(currentId)!;
const queue = getTrackQueue(track);
if (!queue) return undefined;
if (origin === AudioOrigin.Search) { if (origin === AudioOrigin.Search) {
const index = queue.indexOf(current); const index = queue.indexOf(currentId);
if (index < 0) return undefined; if (index < 0) return undefined;
const direction = isReverseOrder ? -1 : 1; const direction = isReverseOrder ? -1 : 1;
return queue[index + direction]; return queue[index + direction];
} }
const { chatId } = parseMessageKey(splitTrackId(current).messageKey); const { chatId } = parseMessageKey(splitTrackId(currentId).messageKey);
const chatAudio = queue.filter((id) => id.startsWith(`msg${chatId}`)); const chatAudio = queue.filter((id) => id.startsWith(`msg${chatId}`));
const index = chatAudio.indexOf(current); const index = chatAudio.indexOf(currentId);
if (index < 0) return undefined; if (index < 0) return undefined;
let direction = origin === AudioOrigin.Inline ? -1 : 1; let direction = origin === AudioOrigin.Inline ? -1 : 1;
if (isReverseOrder) direction *= -1; if (isReverseOrder) direction *= -1;