From 6cfe2a09df6676a9a5487c47a43fea02de158929 Mon Sep 17 00:00:00 2001 From: Alvaro <110414366+alvaro-signal@users.noreply.github.com> Date: Thu, 15 Sep 2022 14:10:46 -0600 Subject: [PATCH] Consecutive playback and per-conversation playback rate --- sounds/state-change_confirm-down.ogg | Bin 0 -> 10298 bytes sounds/state-change_confirm-up.ogg | Bin 0 -> 9978 bytes test/setup-test-node.js | 7 + ts/components/GlobalAudioContext.tsx | 18 +- .../conversation/Message.stories.tsx | 100 ++++- ts/components/conversation/Message.tsx | 3 + ts/components/conversation/MessageAudio.tsx | 297 ++++----------- ts/model-types.d.ts | 1 + ts/models/conversations.ts | 1 + ts/services/globalMessageAudio.ts | 85 +++++ ts/shims/dispatchItemsMiddleware.ts | 11 +- ts/state/ducks/audioPlayer.ts | 350 ++++++++++++++++-- ts/state/ducks/conversations.ts | 41 ++ ts/state/selectors/audioPlayer.ts | 64 +++- ts/state/smart/MessageAudio.tsx | 21 +- ts/state/smart/renderAudioAttachment.tsx | 4 +- ts/test-both/state/ducks/audioPlayer_test.ts | 48 +-- .../state/selectors/audioPlayer_test.ts | 27 +- ts/util/iterables.ts | 10 + ts/util/lint/exceptions.json | 14 + 20 files changed, 783 insertions(+), 319 deletions(-) create mode 100755 sounds/state-change_confirm-down.ogg create mode 100755 sounds/state-change_confirm-up.ogg create mode 100644 ts/services/globalMessageAudio.ts diff --git a/sounds/state-change_confirm-down.ogg b/sounds/state-change_confirm-down.ogg new file mode 100755 index 0000000000000000000000000000000000000000..b2b5d58fc10e22ab943b488968287d868bd770fe GIT binary patch literal 10298 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 literal 0 HcmV?d00001 diff --git a/test/setup-test-node.js b/test/setup-test-node.js index 845705847..4e030043a 100644 --- a/test/setup-test-node.js +++ b/test/setup-test-node.js @@ -37,3 +37,10 @@ global.window = { // For ducks/network.getEmptyState() global.navigator = {}; global.WebSocket = {}; + +// For GlobalAudioContext.tsx +/* eslint max-classes-per-file: ["error", 2] */ +global.AudioContext = class {}; +global.Audio = class { + addEventListener() {} +}; diff --git a/ts/components/GlobalAudioContext.tsx b/ts/components/GlobalAudioContext.tsx index 9453dd617..cd2cfff84 100644 --- a/ts/components/GlobalAudioContext.tsx +++ b/ts/components/GlobalAudioContext.tsx @@ -1,4 +1,4 @@ -// Copyright 2021 Signal Messenger, LLC +// Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import * as React from 'react'; @@ -18,7 +18,6 @@ export type ComputePeaksResult = { }; export type Contents = { - audio: HTMLAudioElement; computePeaks(url: string, barCount: number): Promise; }; @@ -168,7 +167,6 @@ export async function computePeaks( } const globalContents: Contents = { - audio: new Audio(), computePeaks, }; @@ -178,6 +176,7 @@ export type GlobalAudioProps = { conversationId: string | undefined; isPaused: boolean; children?: React.ReactNode | React.ReactChildren; + unloadMessageAudio: () => void; }; /** @@ -186,22 +185,15 @@ export type GlobalAudioProps = { */ export const GlobalAudioProvider: React.FC = ({ conversationId, - isPaused, children, + unloadMessageAudio, }) => { // When moving between conversations - stop audio React.useEffect(() => { return () => { - globalContents.audio.pause(); + unloadMessageAudio(); }; - }, [conversationId]); - - // Pause when requested by parent - React.useEffect(() => { - if (isPaused) { - globalContents.audio.pause(); - } - }, [isPaused]); + }, [conversationId, unloadMessageAudio]); return ( diff --git a/ts/components/conversation/Message.stories.tsx b/ts/components/conversation/Message.stories.tsx index 93c847199..a848c4bca 100644 --- a/ts/components/conversation/Message.stories.tsx +++ b/ts/components/conversation/Message.stories.tsx @@ -116,24 +116,99 @@ const renderEmojiPicker: Props['renderEmojiPicker'] = ({ const renderReactionPicker: Props['renderReactionPicker'] = () =>
; -const MessageAudioContainer: React.FC = props => { - const [active, setActive] = React.useState<{ - id?: string; - context?: string; - }>({}); - const audio = React.useMemo(() => new Audio(), []); +/** + * It doesn't handle consecutive playback + * since that logic mostly lives in the audioPlayer duck + */ +const MessageAudioContainer: React.FC = ({ + played, + ...props +}) => { + const [isActive, setIsActive] = React.useState(false); + const [currentTime, setCurrentTime] = React.useState(0); + const [playbackRate, setPlaybackRate] = React.useState(1); + const [playing, setPlaying] = React.useState(false); + const [_played, setPlayed] = React.useState(played); + + const audio = React.useMemo(() => { + const a = new Audio(); + + a.addEventListener('timeupdate', () => { + setCurrentTime(a.currentTime); + }); + + a.addEventListener('ended', () => { + setIsActive(false); + }); + + a.addEventListener('loadeddata', () => { + a.currentTime = currentTime; + }); + + return a; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const loadAndPlayMessageAudio = ( + _id: string, + url: string, + _context: string, + position: number + ) => { + if (!active) { + audio.src = url; + setIsActive(true); + } + if (!playing) { + audio.play(); + setPlaying(true); + } + audio.currentTime = audio.duration * position; + if (!Number.isNaN(audio.currentTime)) { + setCurrentTime(audio.currentTime); + } + }; + + const setPlaybackRateAction = (_conversationId: string, rate: number) => { + audio.playbackRate = rate; + setPlaybackRate(rate); + }; + + const setIsPlayingAction = (value: boolean) => { + if (value) { + audio.play(); + } else { + audio.pause(); + } + setPlaying(value); + }; + + const setCurrentTimeAction = (value: number) => { + audio.currentTime = value; + setCurrentTime(currentTime); + }; + + const active = isActive + ? { playing, playbackRate, currentTime, duration: audio.duration } + : undefined; + + const setPlayedAction = () => { + setPlayed(true); + }; return ( setActive({ id, context })} - onFirstPlayed={action('onFirstPlayed')} - activeAudioID={active.id} - activeAudioContext={active.context} + active={active} + played={_played} + loadAndPlayMessageAudio={loadAndPlayMessageAudio} + onFirstPlayed={setPlayedAction} + setIsPlaying={setIsPlayingAction} + setPlaybackRate={setPlaybackRateAction} + setCurrentTime={setCurrentTimeAction} /> ); }; @@ -1263,6 +1338,7 @@ export const _Audio = (): JSX.Element => { contentType: AUDIO_MP3, fileName: 'incompetech-com-Agnus-Dei-X.mp3', url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3', + path: 'somepath', }), ], ...(isPlayed @@ -1305,6 +1381,7 @@ LongAudio.args = { contentType: AUDIO_MP3, fileName: 'long-audio.mp3', url: '/fixtures/long-audio.mp3', + path: 'somepath', }), ], status: 'sent', @@ -1317,6 +1394,7 @@ AudioWithCaption.args = { contentType: AUDIO_MP3, fileName: 'incompetech-com-Agnus-Dei-X.mp3', url: '/fixtures/incompetech-com-Agnus-Dei-X.mp3', + path: 'somepath', }), ], status: 'sent', diff --git a/ts/components/conversation/Message.tsx b/ts/components/conversation/Message.tsx index fd7a49548..b7f15ab93 100644 --- a/ts/components/conversation/Message.tsx +++ b/ts/components/conversation/Message.tsx @@ -170,6 +170,7 @@ export type AudioAttachmentProps = { expirationLength?: number; expirationTimestamp?: number; id: string; + conversationId: string; played: boolean; showMessageDetail: (id: string) => void; status?: MessageStatusType; @@ -898,6 +899,7 @@ export class Message extends React.PureComponent { expirationTimestamp, i18n, id, + conversationId, isSticker, kickOffAttachmentDownload, markAttachmentAsCorrupted, @@ -1044,6 +1046,7 @@ export class Message extends React.PureComponent { expirationLength, expirationTimestamp, id, + conversationId, played, showMessageDetail, status, diff --git a/ts/components/conversation/MessageAudio.tsx b/ts/components/conversation/MessageAudio.tsx index ae3d6a2a7..3adec475f 100644 --- a/ts/components/conversation/MessageAudio.tsx +++ b/ts/components/conversation/MessageAudio.tsx @@ -1,28 +1,22 @@ // Copyright 2021-2022 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import React, { - useRef, - useEffect, - useState, - useReducer, - useCallback, -} from 'react'; +import React, { useRef, useEffect, useState } from 'react'; import classNames from 'classnames'; import { noop } from 'lodash'; -import { assertDev } from '../../util/assert'; import type { LocalizerType } from '../../types/Util'; import type { AttachmentType } from '../../types/Attachment'; import { isDownloaded } from '../../types/Attachment'; -import { missingCaseError } from '../../util/missingCaseError'; import type { DirectionType, MessageStatusType } from './Message'; import type { ComputePeaksResult } from '../GlobalAudioContext'; import { MessageMetadata } from './MessageMetadata'; import * as log from '../../logging/log'; +import type { ActiveAudioPlayerStateType } from '../../state/ducks/audioPlayer'; -export type Props = { +export type OwnProps = Readonly<{ + active: ActiveAudioPlayerStateType | undefined; renderingContext: string; i18n: LocalizerType; attachment: AttachmentType; @@ -35,25 +29,33 @@ export type Props = { expirationLength?: number; expirationTimestamp?: number; id: string; + conversationId: string; played: boolean; showMessageDetail: (id: string) => void; status?: MessageStatusType; textPending?: boolean; timestamp: number; - - // See: GlobalAudioContext.tsx - audio: HTMLAudioElement; - buttonRef: React.RefObject; kickOffAttachmentDownload(): void; onCorrupted(): void; onFirstPlayed(): void; - computePeaks(url: string, barCount: number): Promise; - activeAudioID: string | undefined; - activeAudioContext: string | undefined; - setActiveAudioID: (id: string | undefined, context: string) => void; -}; +}>; + +export type DispatchProps = Readonly<{ + loadAndPlayMessageAudio: ( + id: string, + url: string, + context: string, + position: number, + isConsecutive: boolean + ) => void; + setCurrentTime: (currentTime: number) => void; + setPlaybackRate: (conversationId: string, rate: number) => void; + setIsPlaying: (value: boolean) => void; +}>; + +export type Props = OwnProps & DispatchProps; type ButtonProps = { i18n: LocalizerType; @@ -142,45 +144,6 @@ const Button: React.FC = props => { ); }; -type StateType = Readonly<{ - isPlaying: boolean; - currentTime: number; - lastAriaTime: number; - playbackRate: number; -}>; - -type ActionType = Readonly< - | { - type: 'SET_IS_PLAYING'; - value: boolean; - } - | { - type: 'SET_CURRENT_TIME'; - value: number; - } - | { - type: 'SET_PLAYBACK_RATE'; - value: number; - } ->; - -function reducer(state: StateType, action: ActionType): StateType { - if (action.type === 'SET_IS_PLAYING') { - return { - ...state, - isPlaying: action.value, - lastAriaTime: state.currentTime, - }; - } - if (action.type === 'SET_CURRENT_TIME') { - return { ...state, currentTime: action.value }; - } - if (action.type === 'SET_PLAYBACK_RATE') { - return { ...state, playbackRate: action.value }; - } - throw missingCaseError(action); -} - /** * Display message audio attachment along with its waveform, duration, and * toggle Play/Pause button. @@ -196,10 +159,12 @@ function reducer(state: StateType, action: ActionType): StateType { */ export const MessageAudio: React.FC = (props: Props) => { const { + active, i18n, renderingContext, attachment, collapseMetadata, + conversationId, withContentAbove, withContentBelow, @@ -217,52 +182,25 @@ export const MessageAudio: React.FC = (props: Props) => { kickOffAttachmentDownload, onCorrupted, onFirstPlayed, - - audio, computePeaks, - - activeAudioID, - activeAudioContext, - setActiveAudioID, + setPlaybackRate, + loadAndPlayMessageAudio, + setCurrentTime, + setIsPlaying, } = props; - assertDev(audio != null, 'GlobalAudioContext always provides audio'); - - const isActive = - activeAudioID === id && activeAudioContext === renderingContext; - const waveformRef = useRef(null); - const [{ isPlaying, currentTime, lastAriaTime, playbackRate }, dispatch] = - useReducer(reducer, { - isPlaying: isActive && !(audio.paused || audio.ended), - currentTime: isActive ? audio.currentTime : 0, - lastAriaTime: isActive ? audio.currentTime : 0, - playbackRate: isActive ? audio.playbackRate : 1, - }); - const setIsPlaying = useCallback( - (value: boolean) => { - dispatch({ type: 'SET_IS_PLAYING', value }); - }, - [dispatch] - ); - - const setCurrentTime = useCallback( - (value: number) => { - dispatch({ type: 'SET_CURRENT_TIME', value }); - }, - [dispatch] - ); - - const setPlaybackRate = useCallback( - (value: number) => { - dispatch({ type: 'SET_PLAYBACK_RATE', value }); - }, - [dispatch] - ); + const isPlaying = active?.playing ?? false; + // if it's playing, use the duration passed as props as it might + // change during loading/playback (?) // NOTE: Avoid division by zero - const [duration, setDuration] = useState(1e-23); + const activeDuration = + active?.duration && !Number.isNaN(active.duration) + ? active.duration + : undefined; + const [duration, setDuration] = useState(activeDuration ?? 1e-23); const [hasPeaks, setHasPeaks] = useState(false); const [peaks, setPeaks] = useState>( @@ -334,122 +272,23 @@ export const MessageAudio: React.FC = (props: Props) => { state, ]); - // This effect attaches/detaches event listeners to the global