From 927225c0bb23c694d25052971dfa7af1fb0a467d Mon Sep 17 00:00:00 2001 From: Reinaldo Neto Date: Wed, 16 Aug 2023 16:52:56 -0300 Subject: [PATCH] change the way we treat the audio --- .../message/Components/Audio/AudioRate.tsx | 14 ++- .../message/Components/Audio/index.tsx | 80 +++++--------- app/lib/methods/handleAudioMedia.ts | 103 ++++++++++++++++++ app/lib/methods/index.ts | 1 + app/views/RoomView/index.tsx | 2 + jest.setup.js | 5 + 6 files changed, 147 insertions(+), 58 deletions(-) create mode 100644 app/lib/methods/handleAudioMedia.ts diff --git a/app/containers/message/Components/Audio/AudioRate.tsx b/app/containers/message/Components/Audio/AudioRate.tsx index ceb672374..a1d528e39 100644 --- a/app/containers/message/Components/Audio/AudioRate.tsx +++ b/app/containers/message/Components/Audio/AudioRate.tsx @@ -5,12 +5,20 @@ import styles from './styles'; import { useTheme } from '../../../../theme'; import Touchable from '../../Touchable'; -const AudioRate = ({ onChange, loaded = false }: { onChange: (value: number) => void; loaded: boolean }) => { - const [rate, setRate] = useState(1.0); +const AudioRate = ({ + onChange, + loaded = false, + rate: rateProps = 1 +}: { + onChange: (value: number) => void; + loaded: boolean; + rate: number; +}) => { + const [rate, setRate] = useState(rateProps); const { colors } = useTheme(); const onPress = () => { - const nextRate = rate === 2.0 ? 0.5 : rate + 0.5; + const nextRate = rate === 2 ? 0.5 : rate + 0.5; setRate(nextRate); onChange(nextRate); }; diff --git a/app/containers/message/Components/Audio/index.tsx b/app/containers/message/Components/Audio/index.tsx index 0d3bfe00c..dbe9c672c 100644 --- a/app/containers/message/Components/Audio/index.tsx +++ b/app/containers/message/Components/Audio/index.tsx @@ -1,8 +1,7 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; import { StyleProp, TextStyle, View } from 'react-native'; -import { AVPlaybackStatus, Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av'; +import { AVPlaybackStatus } from 'expo-av'; import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake'; -import { Sound } from 'expo-av/build/Audio/Sound'; import { useSharedValue } from 'react-native-reanimated'; import Markdown from '../../../markdown'; @@ -11,13 +10,12 @@ import { TGetCustomEmoji } from '../../../../definitions/IEmoji'; import { IAttachment, IUserMessage } from '../../../../definitions'; import { useTheme } from '../../../../theme'; import { downloadMediaFile, getMediaCache } from '../../../../lib/methods/handleMediaDownload'; -import EventEmitter from '../../../../lib/methods/helpers/events'; -import { PAUSE_AUDIO } from '../../constants'; import { fetchAutoDownloadEnabled } from '../../../../lib/methods/autoDownloadPreference'; import styles from './styles'; import Slider from './Slider'; import AudioRate from './AudioRate'; import PlayButton from './PlayButton'; +import handleAudioMedia from '../../../../lib/methods/handleAudioMedia'; interface IMessageAudioProps { file: IAttachment; @@ -27,16 +25,6 @@ interface IMessageAudioProps { author?: IUserMessage; } -const mode = { - allowsRecordingIOS: false, - playsInSilentModeIOS: true, - staysActiveInBackground: true, - shouldDuckAndroid: true, - playThroughEarpieceAndroid: false, - interruptionModeIOS: InterruptionModeIOS.DoNotMix, - interruptionModeAndroid: InterruptionModeAndroid.DoNotMix -}; - const MessageAudio = ({ file, getCustomEmoji, author, isReply, style }: IMessageAudioProps) => { const [loading, setLoading] = useState(true); const [paused, setPaused] = useState(true); @@ -48,10 +36,12 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style }: IMessage const { baseUrl, user } = useContext(MessageContext); const { colors } = useTheme(); - const sound = useRef(null); + const audioUri = useRef(''); + const rate = useRef(1); const onPlaybackStatusUpdate = (status: AVPlaybackStatus) => { if (status) { + onPlaying(status); onLoad(status); onProgress(status); onEnd(status); @@ -59,15 +49,24 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style }: IMessage }; const loadAudio = async (audio: string) => { - const { sound: soundLoaded } = await Audio.Sound.createAsync({ uri: audio }); - sound.current = soundLoaded; - sound.current.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate); + await handleAudioMedia.loadAudio(audio); + audioUri.current = audio; + handleAudioMedia.setOnPlaybackStatusUpdate(audio, onPlaybackStatusUpdate); + }; + + const onPlaying = (data: AVPlaybackStatus) => { + if (data.isLoaded && data.isPlaying) { + setPaused(false); + } else { + setPaused(true); + } }; const onLoad = (data: AVPlaybackStatus) => { if (data.isLoaded && data.durationMillis) { const durationSeconds = data.durationMillis / 1000; duration.value = durationSeconds > 0 ? durationSeconds : 0; + rate.current = data.rate; } }; @@ -80,13 +79,11 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style }: IMessage } }; - const onEnd = async (data: AVPlaybackStatus) => { + const onEnd = (data: AVPlaybackStatus) => { if (data.isLoaded) { if (data.didJustFinish) { try { - await sound.current?.stopAsync(); setPaused(true); - EventEmitter.removeListener(PAUSE_AUDIO, pauseSound.current); currentTime.value = 0; } catch { // do nothing @@ -96,15 +93,9 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style }: IMessage }; const setPosition = async (time: number) => { - await sound.current?.setPositionAsync(time); + await handleAudioMedia.setPositionAsync(audioUri.current, time); }; - const pauseSound = useRef(() => { - EventEmitter.removeListener(PAUSE_AUDIO, pauseSound.current); - setPaused(true); - playPause(true); - }); - const getUrl = () => { let url = file.audio_url; if (url && !url.startsWith('http')) { @@ -113,21 +104,12 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style }: IMessage return url; }; - const togglePlayPause = () => { - setPaused(!paused); - playPause(!paused); - }; - - const playPause = async (isPaused: boolean) => { + const togglePlayPause = async () => { try { - if (isPaused) { - await sound.current?.pauseAsync(); - EventEmitter.removeListener(PAUSE_AUDIO, pauseSound.current); + if (!paused) { + await handleAudioMedia.pauseAudio(audioUri.current); } else { - EventEmitter.emit(PAUSE_AUDIO); - EventEmitter.addEventListener(PAUSE_AUDIO, pauseSound.current); - await Audio.setAudioModeAsync(mode); - await sound.current?.playAsync(); + await handleAudioMedia.playAudio(audioUri.current); } } catch { // Do nothing @@ -135,7 +117,7 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style }: IMessage }; const setRate = async (value = 1.0) => { - await sound.current?.setRateAsync(value, true); + await handleAudioMedia.setRateAsync(audioUri.current, value); }; const handleDownload = async () => { @@ -207,18 +189,6 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style }: IMessage await handleAutoDownload(); }; handleCache(); - - return () => { - EventEmitter.removeListener(PAUSE_AUDIO, pauseSound.current); - const unloadAsync = async () => { - try { - await sound.current?.unloadAsync(); - } catch { - // Do nothing - } - }; - unloadAsync(); - }; }, []); useEffect(() => { @@ -243,7 +213,7 @@ const MessageAudio = ({ file, getCustomEmoji, author, isReply, style }: IMessage > - + ); diff --git a/app/lib/methods/handleAudioMedia.ts b/app/lib/methods/handleAudioMedia.ts new file mode 100644 index 000000000..869b54ef4 --- /dev/null +++ b/app/lib/methods/handleAudioMedia.ts @@ -0,0 +1,103 @@ +import { AVPlaybackStatus, Audio, InterruptionModeAndroid, InterruptionModeIOS } from 'expo-av'; +import { Sound } from 'expo-av/build/Audio/Sound'; + +const mode = { + allowsRecordingIOS: false, + playsInSilentModeIOS: true, + staysActiveInBackground: true, + shouldDuckAndroid: true, + playThroughEarpieceAndroid: false, + interruptionModeIOS: InterruptionModeIOS.DoNotMix, + interruptionModeAndroid: InterruptionModeAndroid.DoNotMix +}; + +class HandleAudioMedia { + private audioQueue: { [uri: string]: Sound }; + private audioPlaying: string; + + constructor() { + this.audioQueue = {}; + this.audioPlaying = ''; + } + + async loadAudio(uri: string): Promise { + if (this.audioQueue[uri]) { + return this.audioQueue[uri]; + } + const { sound } = await Audio.Sound.createAsync({ uri }); + this.audioQueue[uri] = sound; + return sound; + } + + onPlaybackStatusUpdate(uri: string, status: AVPlaybackStatus, callback: (status: AVPlaybackStatus) => void) { + if (status) { + callback(status); + this.onEnd(uri, status); + } + } + + async onEnd(uri: string, status: AVPlaybackStatus) { + if (status.isLoaded) { + if (status.didJustFinish) { + try { + await this.audioQueue[uri]?.stopAsync(); + this.audioPlaying = ''; + } catch { + // do nothing + } + } + } + } + + setOnPlaybackStatusUpdate(uri: string, callback: (status: AVPlaybackStatus) => void): void { + return this.audioQueue[uri]?.setOnPlaybackStatusUpdate(status => this.onPlaybackStatusUpdate(uri, status, callback)); + } + + async playAudio(uri: string) { + if (this.audioPlaying) { + await this.pauseAudio(this.audioPlaying); + } + await Audio.setAudioModeAsync(mode); + await this.audioQueue[uri]?.playAsync(); + this.audioPlaying = uri; + } + + async pauseAudio(uri: string) { + await this.audioQueue[uri]?.pauseAsync(); + this.audioPlaying = ''; + } + + async setPositionAsync(uri: string, time: number) { + try { + await this.audioQueue[uri]?.setPositionAsync(time); + } catch { + // Do nothing + // It's returning a error with this code: E_AV_SEEKING, however it's working as expected + } + } + + async setRateAsync(uri: string, value = 1.0) { + await this.audioQueue[uri].setRateAsync(value, true); + } + + async unloadAudio(uri: string) { + await this.audioQueue[uri]?.stopAsync(); + await this.audioQueue[uri]?.unloadAsync(); + this.audioPlaying = ''; + } + + async unloadAllAudios() { + const audiosLoaded = Object.values(this.audioQueue); + await Promise.allSettled( + audiosLoaded.map(async audio => { + await audio?.stopAsync(); + await audio?.unloadAsync(); + }) + ); + this.audioPlaying = ''; + this.audioQueue = {}; + } +} + +const handleAudioMedia = new HandleAudioMedia(); +export default handleAudioMedia; diff --git a/app/lib/methods/index.ts b/app/lib/methods/index.ts index 602d78f16..7273a71af 100644 --- a/app/lib/methods/index.ts +++ b/app/lib/methods/index.ts @@ -37,3 +37,4 @@ export * from './crashReport'; export * from './parseSettings'; export * from './subscribeRooms'; export * from './serializeAsciiUrl'; +export * from './handleAudioMedia'; diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 75fc7c65d..cbf737e47 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -93,6 +93,7 @@ import { import { Services } from '../../lib/services'; import { withActionSheet, IActionSheetProvider } from '../../containers/ActionSheet'; import { goRoom, TGoRoomItem } from '../../lib/methods/helpers/goRoom'; +import handleAudioMedia from '../../lib/methods/handleAudioMedia'; type TStateAttrsUpdate = keyof IRoomViewState; @@ -411,6 +412,7 @@ class RoomView extends React.Component { const { editing, room } = this.state; const db = database.active; this.mounted = false; + await handleAudioMedia.unloadAllAudios(); if (!editing && this.messagebox && this.messagebox.current) { const { text } = this.messagebox.current; let obj: TSubscriptionModel | TThreadModel | null = null; diff --git a/jest.setup.js b/jest.setup.js index 2e2d78bc3..856e15ca2 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -69,3 +69,8 @@ jest.mock('react-native-math-view', () => { MathText: react.View // {...} Named export }; }); + +jest.mock('expo-av', () => ({ + InterruptionModeIOS: { DoNotMix: 1 }, + InterruptionModeAndroid: { DoNotMix: 1 } +}));