signal-desktop/ts/state/smart/VoiceNotesPlaybackProvider.tsx
2023-09-06 11:06:30 -07:00

239 lines
6.8 KiB
TypeScript

// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import React, { useEffect } from 'react';
import { useSelector } from 'react-redux';
import type { VoiceNotesPlaybackProps } from '../../components/VoiceNotesPlaybackContext';
import { VoiceNotesPlaybackProvider } from '../../components/VoiceNotesPlaybackContext';
import { selectAudioPlayerActive } from '../selectors/audioPlayer';
import {
AudioPlayerContent,
useAudioPlayerActions,
} from '../ducks/audioPlayer';
import { globalMessageAudio } from '../../services/globalMessageAudio';
import { strictAssert } from '../../util/assert';
import { drop } from '../../util/drop';
import * as log from '../../logging/log';
import { Sound, SoundType } from '../../util/Sound';
import { getConversations } from '../selectors/conversations';
import { SeenStatus } from '../../MessageSeenStatus';
import { markViewed } from '../ducks/conversations';
import * as Errors from '../../types/errors';
import { usePrevious } from '../../hooks/usePrevious';
const stateChangeConfirmDownSound = new Sound({
soundType: SoundType.VoiceNoteStart,
});
const stateChangeConfirmUpSound = new Sound({
soundType: SoundType.VoiceNoteEnd,
});
/**
* Synchronizes the audioPlayer redux state with globalMessageAudio
*/
export function SmartVoiceNotesPlaybackProvider(
props: VoiceNotesPlaybackProps
): JSX.Element | null {
const active = useSelector(selectAudioPlayerActive);
const conversations = useSelector(getConversations);
const previousStartPosition = usePrevious(undefined, active?.startPosition);
const content = active?.content;
let url: undefined | string;
let messageId: undefined | string;
let messageIdForLogging: undefined | string;
let playNextConsecutiveSound = false;
let playFinishConsecutiveSound = false;
if (content && AudioPlayerContent.isVoiceNote(content)) {
({ url, id: messageId } = content.current);
messageIdForLogging = content.current.messageIdForLogging;
playNextConsecutiveSound = content.isConsecutive;
playFinishConsecutiveSound =
content.isConsecutive && content.queue.length === 0;
}
if (content && AudioPlayerContent.isDraft(content)) {
url = content.url;
}
const {
messageAudioEnded,
currentTimeUpdated,
durationChanged,
unloadMessageAudio,
} = useAudioPlayerActions();
useEffect(() => {
// if we don't have a new audio source
// just control playback
if (!content || !url || url === globalMessageAudio.url) {
if (!active?.playing && globalMessageAudio.playing) {
globalMessageAudio.pause();
}
if (active?.playing && !globalMessageAudio.playing) {
globalMessageAudio.play();
}
if (active && active.playbackRate !== globalMessageAudio.playbackRate) {
globalMessageAudio.playbackRate = active.playbackRate;
}
if (
active &&
active.startPosition !== undefined &&
active.startPosition !== previousStartPosition &&
globalMessageAudio.duration !== undefined
) {
globalMessageAudio.currentTime =
active.startPosition * globalMessageAudio.duration;
}
if (!active?.playing && globalMessageAudio.playing) {
globalMessageAudio.pause();
}
if (active?.playing && !globalMessageAudio.playing) {
globalMessageAudio.play();
}
if (active && active.playbackRate !== globalMessageAudio.playbackRate) {
globalMessageAudio.playbackRate = active.playbackRate;
}
// if user requested a new position
if (
active &&
active.startPosition !== undefined &&
active.startPosition !== previousStartPosition &&
active.duration
) {
globalMessageAudio.currentTime = active.startPosition * active.duration;
}
return;
}
// if we have a new audio source
loadAudio({
url,
playbackRate: active.playbackRate,
messageId,
messageIdForLogging,
startPosition: active.startPosition,
playFinishConsecutiveSound,
durationChanged,
unloadMessageAudio,
currentTimeUpdated,
messageAudioEnded,
});
if (playNextConsecutiveSound) {
drop(
(async () => {
await stateChangeConfirmDownSound.play();
globalMessageAudio.play();
})()
);
} else {
globalMessageAudio.play();
}
if (AudioPlayerContent.isVoiceNote(content)) {
if (!content.current.isPlayed) {
const message = conversations.messagesLookup[content.current.id];
if (message && message.seenStatus !== SeenStatus.Unseen) {
markViewed(content.current.id);
}
} else {
log.info('SmartVoiceNotesPlaybackProvider: message already played', {
message: content.current.messageIdForLogging,
});
}
}
}, [
active,
content,
conversations.messagesLookup,
currentTimeUpdated,
durationChanged,
messageAudioEnded,
messageId,
messageIdForLogging,
playFinishConsecutiveSound,
playNextConsecutiveSound,
previousStartPosition,
unloadMessageAudio,
url,
]);
return <VoiceNotesPlaybackProvider {...props} />;
}
function loadAudio({
url,
playbackRate,
messageId,
messageIdForLogging,
startPosition,
playFinishConsecutiveSound,
durationChanged,
currentTimeUpdated,
messageAudioEnded,
unloadMessageAudio,
}: {
url: string;
playbackRate: number;
messageId: string | undefined;
messageIdForLogging: string | undefined;
startPosition: number;
playFinishConsecutiveSound: boolean;
durationChanged: (value: number | undefined) => void;
currentTimeUpdated: (value: number) => void;
messageAudioEnded: () => void;
unloadMessageAudio: () => void;
}) {
globalMessageAudio.load({
url,
playbackRate,
onLoadedMetadata() {
strictAssert(
globalMessageAudio.duration !== undefined,
'Audio should have definite duration on `loadedmetadata` event'
);
log.info(
'SmartVoiceNotesPlaybackProvider: `loadedmetadata` event',
messageId
);
if (startPosition !== 0) {
globalMessageAudio.currentTime =
startPosition * globalMessageAudio.duration;
}
durationChanged(globalMessageAudio.duration);
},
onDurationChange() {
log.info(
'SmartVoiceNotesPlaybackProvider: `durationchange` event',
messageId
);
durationChanged(globalMessageAudio.duration);
},
onTimeUpdate() {
currentTimeUpdated(globalMessageAudio.currentTime);
},
onEnded() {
if (playFinishConsecutiveSound) {
drop(stateChangeConfirmUpSound.play());
}
messageAudioEnded();
},
onError(error) {
log.error(
'SmartVoiceNotesPlaybackProvider: playback error',
messageIdForLogging,
Errors.toLogFormat(error)
);
unloadMessageAudio();
},
});
}