signal-desktop/ts/state/smart/VoiceNotesPlaybackProvider.tsx
2023-02-28 06:07:40 -07:00

154 lines
5 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 { useAudioPlayerActions } from '../ducks/audioPlayer';
import { globalMessageAudio } from '../../services/globalMessageAudio';
import { strictAssert } from '../../util/assert';
import * as log from '../../logging/log';
import { Sound } 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 stateChangeConfirmUpSound = new Sound({
src: 'sounds/state-change_confirm-up.ogg',
});
const stateChangeConfirmDownSound = new Sound({
src: 'sounds/state-change_confirm-down.ogg',
});
/**
* 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;
const current = content?.current;
const url = current?.url;
const {
messageAudioEnded,
currentTimeUpdated,
durationChanged,
unloadMessageAudio,
} = useAudioPlayerActions();
useEffect(() => {
// if we don't have a new audio source
// just control playback
if (!content || !current || !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.currentTime =
active.startPosition * globalMessageAudio.duration;
}
return;
}
// otherwise we have a new audio source
// we just load it and play it
globalMessageAudio.load({
url,
playbackRate: active.playbackRate,
onLoadedMetadata() {
strictAssert(
!Number.isNaN(globalMessageAudio.duration),
'Audio should have definite duration on `loadedmetadata` event'
);
log.info(
'SmartVoiceNotesPlaybackProvider: `loadedmetadata` event',
current.id
);
if (active.startPosition !== 0) {
globalMessageAudio.currentTime =
active.startPosition * globalMessageAudio.duration;
}
},
onDurationChange() {
log.info(
'SmartVoiceNotesPlaybackProvider: `durationchange` event',
current.id
);
const reportedDuration = globalMessageAudio.duration;
// the underlying Audio element can return NaN if the audio hasn't loaded
// we filter out 0 or NaN as they are not useful values downstream
const newDuration =
Number.isNaN(reportedDuration) || reportedDuration === 0
? undefined
: reportedDuration;
durationChanged(newDuration);
},
onTimeUpdate() {
currentTimeUpdated(globalMessageAudio.currentTime);
},
onEnded() {
if (content.isConsecutive && content.queue.length === 0) {
void stateChangeConfirmDownSound.play();
}
messageAudioEnded();
},
onError(error) {
log.error(
'SmartVoiceNotesPlaybackProvider: playback error',
current.messageIdForLogging,
Errors.toLogFormat(error)
);
unloadMessageAudio();
},
});
// if this message was part of the queue (consecutive, added indirectly)
// we play a note to let the user we're onto a new message
// (false for the first message in a consecutive group, since the user initiated it)
if (content.isConsecutive) {
// eslint-disable-next-line more/no-then
void stateChangeConfirmUpSound.play().then(() => {
globalMessageAudio.play();
});
} else {
globalMessageAudio.play();
}
if (!current.isPlayed) {
const message = conversations.messagesLookup[current.id];
if (message && message.seenStatus !== SeenStatus.Unseen) {
markViewed(current.id);
}
} else {
log.info('SmartVoiceNotesPlaybackProvider: message already played', {
message: current.messageIdForLogging,
});
}
});
return <VoiceNotesPlaybackProvider {...props} />;
}