236 lines
6.8 KiB
TypeScript
236 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 * 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 stateChangeConfirmUpSound = new Sound({
|
|
soundType: SoundType.VoiceNoteStart,
|
|
});
|
|
const stateChangeConfirmDownSound = 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) {
|
|
// eslint-disable-next-line more/no-then
|
|
void stateChangeConfirmUpSound.play().then(() => {
|
|
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) {
|
|
void stateChangeConfirmDownSound.play();
|
|
}
|
|
messageAudioEnded();
|
|
},
|
|
onError(error) {
|
|
log.error(
|
|
'SmartVoiceNotesPlaybackProvider: playback error',
|
|
messageIdForLogging,
|
|
Errors.toLogFormat(error)
|
|
);
|
|
unloadMessageAudio();
|
|
},
|
|
});
|
|
}
|