154 lines
5 KiB
TypeScript
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} />;
|
|
}
|