From 8d8f7b9bcdad5fb66b85437668b09aba6b400b53 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 19 Oct 2024 18:58:53 -0400 Subject: [PATCH 01/20] fix: fix leaking promise on getDuration + NSError() leak rejection --- ios/AudioRecorder.swift | 6 +++--- ios/AudioWaveform.swift | 24 ++++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/ios/AudioRecorder.swift b/ios/AudioRecorder.swift index f2b4e76..1a18936 100644 --- a/ios/AudioRecorder.swift +++ b/ios/AudioRecorder.swift @@ -49,7 +49,7 @@ public class AudioRecorder: NSObject, AVAudioRecorderDelegate{ if (path == nil) { guard let newPath = self.createAudioRecordPath(fileNameFormat: fileNameFormat) else { - reject(Constants.audioWaveforms, "Failed to initialise file URL", NSError()) + reject(Constants.audioWaveforms, "Failed to initialise file URL", nil) return } audioUrl = newPath @@ -62,7 +62,7 @@ public class AudioRecorder: NSObject, AVAudioRecorderDelegate{ try AVAudioSession.sharedInstance().setCategory(AVAudioSession.Category.playAndRecord, options: options) try AVAudioSession.sharedInstance().setActive(true) guard let newPath = audioUrl else { - reject(Constants.audioWaveforms, "Failed to initialise file URL", NSError()) + reject(Constants.audioWaveforms, "Failed to initialise file URL", nil) return } audioRecorder = try AVAudioRecorder(url: newPath, settings: settings as [String : Any]) @@ -116,7 +116,7 @@ public class AudioRecorder: NSObject, AVAudioRecorderDelegate{ resolve([asset.url.absoluteString,Int(recordedDuration.seconds * 1000).description]) } } else { - reject(Constants.audioWaveforms, "Failed to stop recording", NSError()) + reject(Constants.audioWaveforms, "Failed to stop recording", nil) } audioRecorder = nil } diff --git a/ios/AudioWaveform.swift b/ios/AudioWaveform.swift index db3b161..28dac0c 100644 --- a/ios/AudioWaveform.swift +++ b/ios/AudioWaveform.swift @@ -105,7 +105,7 @@ class AudioWaveform: RCTEventEmitter { if(key != nil) { createOrUpdateExtractor(playerKey: key!, path: path, noOfSamples: noOfSamples, resolve: resolve, rejecter: reject) } else { - reject(Constants.audioWaveforms,"Can not get waveform data",NSError()) + reject(Constants.audioWaveforms,"Can not get waveform data",nil) } } @@ -114,7 +114,7 @@ class AudioWaveform: RCTEventEmitter { do { let audioUrl = URL.init(string: path!) if(audioUrl == nil){ - reject(Constants.audioWaveforms, "Failed to initialise Url from provided audio file If path contains `file://` try removing it", NSError()) + reject(Constants.audioWaveforms, "Failed to initialise Url from provided audio file If path contains `file://` try removing it", nil) } let newExtractor = try WaveformExtractor(url: audioUrl!, channel: self, resolve: resolve, rejecter: reject) extractors[playerKey] = newExtractor @@ -130,7 +130,7 @@ class AudioWaveform: RCTEventEmitter { reject(Constants.audioWaveforms, "Failed to decode audio file", e) } } else { - reject(Constants.audioWaveforms, "Audio file path can't be empty or null", NSError()) + reject(Constants.audioWaveforms, "Audio file path can't be empty or null", nil) } } @@ -156,7 +156,7 @@ class AudioWaveform: RCTEventEmitter { resolver: resolve, rejecter: reject) } else { - reject(Constants.audioWaveforms, "Can not prepare player", NSError()) + reject(Constants.audioWaveforms, "Can not prepare player", nil) } } @@ -168,7 +168,7 @@ class AudioWaveform: RCTEventEmitter { if(key != nil){ audioPlayers[key!]?.startPlyer(finishMode, speed: speed, result:resolve) } else { - reject(Constants.audioWaveforms, "Can not start player", NSError()) + reject(Constants.audioWaveforms, "Can not start player", nil) } } @@ -177,7 +177,7 @@ class AudioWaveform: RCTEventEmitter { if(key != nil){ audioPlayers[key!]?.pausePlayer(result: resolve) } else { - reject(Constants.audioWaveforms, "Can not pause player, Player key is null", NSError()) + reject(Constants.audioWaveforms, "Can not pause player, Player key is null", nil) } } @@ -188,7 +188,7 @@ class AudioWaveform: RCTEventEmitter { audioPlayers[key!] = nil // Release the player after stopping it resolve(true) } else { - reject(Constants.audioWaveforms, "Can not stop player, Player key is null", NSError()) + reject(Constants.audioWaveforms, "Can not stop player, Player key is null", nil) } } @@ -197,7 +197,7 @@ class AudioWaveform: RCTEventEmitter { if(key != nil){ audioPlayers[key!]?.seekTo(args?[Constants.progress] as? Double,resolve) } else { - reject(Constants.audioWaveforms, "Can not seek to postion, Player key is null", NSError()) + reject(Constants.audioWaveforms, "Can not seek to postion, Player key is null", nil) } } @@ -206,14 +206,14 @@ class AudioWaveform: RCTEventEmitter { if(key != nil){ audioPlayers[key!]?.setVolume(args?[Constants.volume] as? Double,resolve) } else { - reject(Constants.audioWaveforms, "Can not set volume, Player key is null", NSError()) + reject(Constants.audioWaveforms, "Can not set volume, Player key is null", nil) } } @objc func getDuration(_ args: NSDictionary?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void { let type = args?[Constants.durationType] as? Int let key = args?[Constants.playerKey] as? String - if(key != nil){ + if(key != nil && audioPlayers[key!] != nil){ do{ if(type == 0) { try audioPlayers[key!]?.getDuration(DurationType.Current,resolve) @@ -224,7 +224,7 @@ class AudioWaveform: RCTEventEmitter { reject(Constants.audioWaveforms, "Failed to get duration", e) } } else { - reject(Constants.audioWaveforms, "Can not get duration", NSError()) + reject(Constants.audioWaveforms, "Can not get duration", nil) } } @@ -263,7 +263,7 @@ class AudioWaveform: RCTEventEmitter { let status = audioPlayers[key!]?.setPlaybackSpeed(speed) resolve(status) } else { - reject(Constants.audioWaveforms, "Can not pause player, Player key is null", NSError()) + reject(Constants.audioWaveforms, "Can not pause player, Player key is null", nil) } } } From 90c3f7bd6606fad18530addb307d8825a5c11e76 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 19 Oct 2024 20:27:35 -0400 Subject: [PATCH 02/20] fix: `startPlayer`, `pausePlayer`, `seekToPlayer`, `setVolume` also leak --- ios/AudioPlayer.swift | 2 +- ios/AudioWaveform.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ios/AudioPlayer.swift b/ios/AudioPlayer.swift index 4f0e48b..6378225 100644 --- a/ios/AudioPlayer.swift +++ b/ios/AudioPlayer.swift @@ -87,7 +87,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { EventEmitter.sharedInstance.dispatch(name: withName, body: body) } - func startPlyer(_ finishMode: Int?, speed: Float, result: RCTPromiseResolveBlock) { + func startPlayer(_ finishMode: Int?, speed: Float, result: RCTPromiseResolveBlock) { if(finishMode != nil && finishMode == 0) { self.finishMode = FinishMode.loop } else if(finishMode != nil && finishMode == 1) { diff --git a/ios/AudioWaveform.swift b/ios/AudioWaveform.swift index 28dac0c..00b42f2 100644 --- a/ios/AudioWaveform.swift +++ b/ios/AudioWaveform.swift @@ -165,8 +165,8 @@ class AudioWaveform: RCTEventEmitter { let finishMode = args?[Constants.finishMode] as? Int let speed = (args?[Constants.speed] as? NSNumber)?.floatValue ?? 1.0 - if(key != nil){ - audioPlayers[key!]?.startPlyer(finishMode, speed: speed, result:resolve) + if(key != nil && audioPlayers[key!] != nil){ + audioPlayers[key!]?.startPlayer(finishMode, speed: speed, result:resolve) } else { reject(Constants.audioWaveforms, "Can not start player", nil) } @@ -174,7 +174,7 @@ class AudioWaveform: RCTEventEmitter { @objc func pausePlayer(_ args: NSDictionary?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void { let key = args?[Constants.playerKey] as? String - if(key != nil){ + if(key != nil && audioPlayers[key!] != nil){ audioPlayers[key!]?.pausePlayer(result: resolve) } else { reject(Constants.audioWaveforms, "Can not pause player, Player key is null", nil) @@ -194,7 +194,7 @@ class AudioWaveform: RCTEventEmitter { @objc func seekToPlayer(_ args: NSDictionary?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void { let key = args?[Constants.playerKey] as? String - if(key != nil){ + if(key != nil && audioPlayers[key!] != nil){ audioPlayers[key!]?.seekTo(args?[Constants.progress] as? Double,resolve) } else { reject(Constants.audioWaveforms, "Can not seek to postion, Player key is null", nil) @@ -203,7 +203,7 @@ class AudioWaveform: RCTEventEmitter { @objc func setVolume(_ args: NSDictionary?, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void { let key = args?[Constants.playerKey] as? String - if(key != nil){ + if(key != nil && audioPlayers[key!] != nil){ audioPlayers[key!]?.setVolume(args?[Constants.volume] as? Double,resolve) } else { reject(Constants.audioWaveforms, "Can not set volume, Player key is null", nil) From de6a55cb29fe3986b71a58fa1281858cbd6e1ef7 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 19 Oct 2024 20:53:57 -0400 Subject: [PATCH 03/20] fix: play/stop button not showing on iOS + recording radius black theme --- example/ios/Podfile.lock | 40 ++++++++++++++++++++++++++++++++++++---- example/package.json | 1 + example/src/App.tsx | 3 ++- example/src/styles.ts | 3 +++ 4 files changed, 42 insertions(+), 5 deletions(-) diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 65d88fd..11db637 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -74,6 +74,18 @@ PODS: - hermes-engine/Pre-built (= 0.72.7) - hermes-engine/Pre-built (0.72.7) - libevent (2.1.12) + - libwebp (1.3.2): + - libwebp/demux (= 1.3.2) + - libwebp/mux (= 1.3.2) + - libwebp/sharpyuv (= 1.3.2) + - libwebp/webp (= 1.3.2) + - libwebp/demux (1.3.2): + - libwebp/webp + - libwebp/mux (1.3.2): + - libwebp/demux + - libwebp/sharpyuv (1.3.2) + - libwebp/webp (1.3.2): + - libwebp/sharpyuv - OpenSSL-Universal (1.1.1100) - RCT-Folly (2021.07.22.00): - boost @@ -378,7 +390,7 @@ PODS: - react-native-audio-waveform (1.0.0): - RCT-Folly (= 2021.07.22.00) - React-Core - - react-native-safe-area-context (4.9.0): + - react-native-safe-area-context (4.11.0): - React-Core - React-NativeModulesApple (0.72.7): - hermes-engine @@ -492,11 +504,21 @@ PODS: - React-perflogger (= 0.72.7) - rn-fetch-blob (0.12.0): - React-Core + - RNFastImage (8.6.3): + - React-Core + - SDWebImage (~> 5.11.1) + - SDWebImageWebPCoder (~> 0.8.4) - RNFS (2.20.0): - React-Core - - RNGestureHandler (2.14.0): + - RNGestureHandler (2.19.0): - RCT-Folly (= 2021.07.22.00) - React-Core + - SDWebImage (5.11.1): + - SDWebImage/Core (= 5.11.1) + - SDWebImage/Core (5.11.1) + - SDWebImageWebPCoder (0.8.5): + - libwebp (~> 1.0) + - SDWebImage/Core (~> 5.10) - SocketRocket (0.6.1) - Yoga (1.14.0) - YogaKit (1.18.1): @@ -568,6 +590,7 @@ DEPENDENCIES: - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) - rn-fetch-blob (from `../node_modules/rn-fetch-blob`) + - RNFastImage (from `../node_modules/react-native-fast-image`) - RNFS (from `../node_modules/react-native-fs`) - RNGestureHandler (from `../node_modules/react-native-gesture-handler`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) @@ -585,7 +608,10 @@ SPEC REPOS: - FlipperKit - fmt - libevent + - libwebp - OpenSSL-Universal + - SDWebImage + - SDWebImageWebPCoder - SocketRocket - YogaKit @@ -673,6 +699,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" rn-fetch-blob: :path: "../node_modules/rn-fetch-blob" + RNFastImage: + :path: "../node_modules/react-native-fast-image" RNFS: :path: "../node_modules/react-native-fs" RNGestureHandler: @@ -698,6 +726,7 @@ SPEC CHECKSUMS: glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: 9180d43df05c1ed658a87cc733dc3044cf90c00a libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 + libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: 83bca1c184feb4d2e51c72c8369b83d641443f95 @@ -715,7 +744,7 @@ SPEC CHECKSUMS: React-jsinspector: 8baadae51f01d867c3921213a25ab78ab4fbcd91 React-logger: 8edc785c47c8686c7962199a307015e2ce9a0e4f react-native-audio-waveform: 7cdb6e4963eeae907240396975b9c79713591758 - react-native-safe-area-context: b97eb6f9e3b7f437806c2ce5983f479f8eb5de4b + react-native-safe-area-context: 851c62c48dce80ccaa5637b6aa5991a1bc36eca9 React-NativeModulesApple: b6868ee904013a7923128892ee4a032498a1024a React-perflogger: 31ea61077185eb1428baf60c0db6e2886f141a5a React-RCTActionSheet: 392090a3abc8992eb269ef0eaa561750588fc39d @@ -734,8 +763,11 @@ SPEC CHECKSUMS: React-utils: 56838edeaaf651220d1e53cd0b8934fb8ce68415 ReactCommon: 5f704096ccf7733b390f59043b6fa9cc180ee4f6 rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba + RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: 32a01c29ecc9bb0b5bf7bc0a33547f61b4dc2741 + RNGestureHandler: 7ad14a6c7b491add489246611d324f10009083ac + SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d + SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a diff --git a/example/package.json b/example/package.json index f8f7cde..48c932f 100644 --- a/example/package.json +++ b/example/package.json @@ -12,6 +12,7 @@ "dependencies": { "react": "18.2.0", "react-native": "0.72.7", + "react-native-fast-image": "^8.6.3", "react-native-fs": "^2.20.0", "react-native-gesture-handler": "^2.13.4", "react-native-safe-area-context": "^4.9.0", diff --git a/example/src/App.tsx b/example/src/App.tsx index 4585946..2369424 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -39,6 +39,7 @@ import { } from './constants'; import stylesheet from './styles'; import { Colors } from './theme'; +import FastImage from 'react-native-fast-image'; const RenderListItem = React.memo( ({ @@ -88,7 +89,7 @@ const RenderListItem = React.memo( {isLoading ? ( ) : ( - StyleSheet.create({ appContainer: { flex: 1, + backgroundColor: useColorScheme() === "dark" ? Colors.gray : Colors.white, }, screenBackground: { flex: 1, From 785b416f52c60ee21aedb144a0f927e7a7d9fdfd Mon Sep 17 00:00:00 2001 From: Prathamesh More Date: Wed, 30 Oct 2024 11:39:55 +0530 Subject: [PATCH 04/20] fix: use partial imports for lodash --- src/components/Waveform/Waveform.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/Waveform/Waveform.tsx b/src/components/Waveform/Waveform.tsx index d156df5..bbde4f2 100644 --- a/src/components/Waveform/Waveform.tsx +++ b/src/components/Waveform/Waveform.tsx @@ -1,4 +1,8 @@ -import { clamp, floor, head, isEmpty, isNil } from 'lodash'; +import clamp from 'lodash/clamp'; +import floor from 'lodash/floor'; +import head from 'lodash/head'; +import isEmpty from 'lodash/isEmpty'; +import isNil from 'lodash/isNil'; import React, { forwardRef, useEffect, From a6b6e519a1116e759b275f754c67dc1d6c462719 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 19 Oct 2024 22:21:04 -0400 Subject: [PATCH 05/20] feat: keep recording on reboot + allow to delete them --- example/src/App.tsx | 43 ++++++++++++++++++++++++++++ example/src/assets/icons/delete.png | Bin 0 -> 7907 bytes example/src/assets/icons/index.ts | 1 + example/src/constants/Audios.ts | 20 +++++++++++-- example/src/styles.ts | 3 +- 5 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 example/src/assets/icons/delete.png diff --git a/example/src/App.tsx b/example/src/App.tsx index 2369424..ce9db4e 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -18,6 +18,7 @@ import React, { } from 'react'; import { ActivityIndicator, + Alert, Image, Linking, Pressable, @@ -35,11 +36,13 @@ import { Icons } from './assets'; import { generateAudioList, playbackSpeedSequence, + getRecordedAudios, type ListItem, } from './constants'; import stylesheet from './styles'; import { Colors } from './theme'; import FastImage from 'react-native-fast-image'; +import fs from 'react-native-fs'; const RenderListItem = React.memo( ({ @@ -248,6 +251,37 @@ const AppContainer = () => { ); }; + const handleDeleteRecordings = async () => { + const recordings = await getRecordedAudios(); + + const deleteRecordings = async () => { + await Promise.all(recordings.map(async recording => fs.unlink(recording))) + .then(() => { + Alert.alert( + 'All recording deleted', + 'All recordings have been deleted successfully! Reboot the app to see the changes.', + [{ text: 'Dismiss' }] + ); + }) + .catch(error => { + Alert.alert( + 'Error deleting recordings', + 'Below error happened while deleting recordings:\n' + error, + [{ text: 'Dismiss' }] + ); + }); + }; + + Alert.alert( + 'Delete all recording', + `Continue to delete all ${recordings.length} recordings.\n App restart is required!`, + [ + { text: 'Cancel', style: 'cancel' }, + { text: 'OK', onPress: deleteRecordings }, + ] + ); + }; + return ( { style={styles.simformImage} resizeMode="contain" /> + + + {list.map(item => ( diff --git a/example/src/assets/icons/delete.png b/example/src/assets/icons/delete.png new file mode 100644 index 0000000000000000000000000000000000000000..68a0eb2eb64cc079be9ba52f858a9c4eddb41cc5 GIT binary patch literal 7907 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 RiKK4;pk4T#rQ3 => { + const recordingSavingPath = Platform.select({ ios: fs.DocumentDirectoryPath, default: fs.CachesDirectoryPath }) + + const items = await fs.readDir(recordingSavingPath) + return items.filter(item => item.path.endsWith('.m4a')).map(item => item.path) +} + /** * Generate a list of file objects with information about successfully copied files (Android) * or all files (iOS). * @returns {Promise} A Promise that resolves to the list of file objects. */ export const generateAudioList = async (): Promise => { - const audioAssets = await copyFilesToNativeResources(); + const audioAssetPaths = (await copyFilesToNativeResources()).map(value => `${filePath}/${value}`); + const recordedAudios = await getRecordedAudios() // Generate the final list based on the copied or available files - return audioAssets?.map?.((value, index) => ({ + return [...audioAssetPaths, ...recordedAudios].map?.((value, index) => ({ fromCurrentUser: index % 2 !== 0, - path: `${filePath}/${value}`, + path: value, })); + }; diff --git a/example/src/styles.ts b/example/src/styles.ts index 088fbfc..6797ff1 100644 --- a/example/src/styles.ts +++ b/example/src/styles.ts @@ -92,7 +92,8 @@ const styles = (params: StyleSheetParams = {}) => }, simformImageContainer: { alignItems: 'center', - justifyContent: 'center', + justifyContent: 'space-around', + flexDirection: 'row', }, loadingText: { color: Colors.black, From 0bfe0c476299a5a3f32a706e0c32196ff90abb5f Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 22 Oct 2024 22:27:33 -0400 Subject: [PATCH 06/20] fix: refresh the list instead of needing to reboot --- example/src/App.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index ce9db4e..893f6db 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -257,11 +257,9 @@ const AppContainer = () => { const deleteRecordings = async () => { await Promise.all(recordings.map(async recording => fs.unlink(recording))) .then(() => { - Alert.alert( - 'All recording deleted', - 'All recordings have been deleted successfully! Reboot the app to see the changes.', - [{ text: 'Dismiss' }] - ); + generateAudioList().then(audioListArray => { + setList(audioListArray); + }); }) .catch(error => { Alert.alert( From 2c3648a021b6c7da8ff5848218ed1082425cbb06 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Tue, 22 Oct 2024 22:57:44 -0400 Subject: [PATCH 07/20] fix: disable delete button when no recording --- example/src/App.tsx | 20 +++++++++++++++++--- example/src/styles.ts | 12 ++++++++++-- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 893f6db..4ac39a8 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -227,6 +227,7 @@ const AppContainer = () => { const [shouldScroll, setShouldScroll] = useState(true); const [currentPlaying, setCurrentPlaying] = useState(''); const [list, setList] = useState([]); + const [nbOfRecording, setNumberOfRecording] = useState(0); const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(1.0); @@ -241,6 +242,12 @@ const AppContainer = () => { }); }, []); + useEffect(() => { + getRecordedAudios().then(recordedAudios => + setNumberOfRecording(recordedAudios.length) + ); + }, [list]); + const changeSpeed = () => { setCurrentPlaybackSpeed( prev => @@ -291,20 +298,27 @@ const AppContainer = () => { - + + style={[ + styles.deleteRecordingContainer, + { opacity: nbOfRecording ? 1 : 0.5 }, + ]} + onPress={handleDeleteRecordings} + disabled={!nbOfRecording}> + + {'Delete recorded audio files'} + diff --git a/example/src/styles.ts b/example/src/styles.ts index 6797ff1..edca6b6 100644 --- a/example/src/styles.ts +++ b/example/src/styles.ts @@ -90,11 +90,19 @@ const styles = (params: StyleSheetParams = {}) => width: '100%', tintColor: Colors.pink, }, - simformImageContainer: { + headerContainer: { + alignItems: 'center', + }, + deleteRecordingContainer: { alignItems: 'center', - justifyContent: 'space-around', flexDirection: 'row', }, + deleteRecordingTitle: { + fontSize: scale(20), + fontWeight: 'bold', + color: Colors.pink, + paddingLeft: scale(8), + }, loadingText: { color: Colors.black, }, From 23365d5e357c890c5aec1700c76b98f4a7e4cb52 Mon Sep 17 00:00:00 2001 From: David Prevost <77302423+dprevost-LMI@users.noreply.github.com> Date: Wed, 23 Oct 2024 07:24:03 -0400 Subject: [PATCH 08/20] Update example/src/App.tsx Co-authored-by: kuldip-simform --- example/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 4ac39a8..22194ce 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -279,7 +279,7 @@ const AppContainer = () => { Alert.alert( 'Delete all recording', - `Continue to delete all ${recordings.length} recordings.\n App restart is required!`, + `Continue to delete all ${recordings.length} recordings.`, [ { text: 'Cancel', style: 'cancel' }, { text: 'OK', onPress: deleteRecordings }, From c164249bcbbc5d63a4861559f7eb24057039c0e9 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Wed, 23 Oct 2024 07:34:30 -0400 Subject: [PATCH 09/20] fix: using pink `tintColor` so it also work on light theme --- example/src/App.tsx | 2 +- example/src/styles.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 22194ce..fb85122 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -313,7 +313,7 @@ const AppContainer = () => { disabled={!nbOfRecording}> diff --git a/example/src/styles.ts b/example/src/styles.ts index edca6b6..8cb9eb3 100644 --- a/example/src/styles.ts +++ b/example/src/styles.ts @@ -54,6 +54,12 @@ const styles = (params: StyleSheetParams = {}) => tintColor: Colors.white, alignSelf: 'flex-end', }, + pinkButtonImage: { + height: scale(22), + width: scale(22), + tintColor: Colors.pink, + alignSelf: 'flex-end', + }, staticWaveformView: { flex: 1, height: scale(75), From 58e83a84b651a9edfd4396198f2fbc7174efcc05 Mon Sep 17 00:00:00 2001 From: prince_dobariya Date: Wed, 13 Nov 2024 12:44:56 +0530 Subject: [PATCH 10/20] fix(UNT-T30512): Issue #129 Handle division by 0 --- src/components/Waveform/Waveform.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Waveform/Waveform.tsx b/src/components/Waveform/Waveform.tsx index d156df5..dcbf604 100644 --- a/src/components/Waveform/Waveform.tsx +++ b/src/components/Waveform/Waveform.tsx @@ -180,7 +180,7 @@ export const Waveform = forwardRef((props, ref) => { const result = await extractWaveformData({ path: path, playerKey: `PlayerFor${path}`, - noOfSamples: noOfSample, + noOfSamples: Math.max(noOfSample, 1), }); (onChangeWaveformLoadState as Function)?.(false); From 7dc0cf8315f6edc5cf3005ad0f1f04a410331ab3 Mon Sep 17 00:00:00 2001 From: nilesh-simform Date: Thu, 14 Nov 2024 12:05:55 +0530 Subject: [PATCH 11/20] fix(UNT-T30467): seek on tap functionality --- example/src/App.tsx | 33 +++++++++++++++++++++++---- example/src/assets/icons/index.ts | 1 + example/src/assets/icons/pause.png | Bin 0 -> 10705 bytes example/src/styles.ts | 5 ++++ src/components/Waveform/Waveform.tsx | 11 ++++++++- 5 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 example/src/assets/icons/pause.png diff --git a/example/src/App.tsx b/example/src/App.tsx index fb85122..63a7b98 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -68,11 +68,17 @@ const RenderListItem = React.memo( const handleButtonAction = () => { if (playerState === PlayerState.stopped) { setCurrentPlaying(item.path); + } else if (playerState === PlayerState.playing) { + ref.current?.pausePlayer(); } else { - setCurrentPlaying(''); + ref.current?.resumePlayer(); } }; + const handleStopAction = () => { + setCurrentPlaying(''); + }; + useEffect(() => { if (currentPlaying !== item.path) { ref.current?.stopPlayer(); @@ -90,19 +96,38 @@ const RenderListItem = React.memo( onPress={handleButtonAction} style={styles.playBackControlPressable}> {isLoading ? ( - + ) : ( )} + + {isLoading ? ( + + ) : ( + + )} + @yCDrTdccYEbCQWEj4R&=u_B=W890Xt@D*531FKOh{ZIa%Gu%4ku{tp%xD&pt0-HTmlCJZlEI>MzbK zZ4xg{6kK|VO?P#!-TvUYYwXHyGk!Rr-1@1abDgioG_}IW+@hb8SEYQ-{w~W9|6_5$ zzm$^O_0b!*{5X--?e5vT=HHw)<^zlYr(WI82-MOI;cF1aM+vjY8 zba<~6-FgrZs<@o5q0)TnshpcT*}IlD#)L`BoYG49y8q=5&Ijtzt3yr|=4AwH@$zi~ zDh^Xz&DW4=KlN0~^^=K9FCEZZ&C8c6y6F|ejBmj!eahy_glX}<8C5tMJfn$XG%Jjj z6r<(iXss|>X^u7(MjJ?@&BD=E^JrgTw1+g>fgJ5Nj}8`$4v~zGAdQZjjgA(M4yKL{ zA&(B6kIpNM&ZdmcA&t(QjZQ3%PN&j;5?Lm_l97RdvmUe-V6;s>+9n@ulaIE^N89A1 zZSv7J`DmMbu(Zi5?L|-L++4}-wX`wXPg-V`){`&KQ|wk<;MyhP-RKnD9F$o&@ha*9 zKR=rwE!{JlZaoSNQU+FO)4vC9+>>*6d+myVSsAHYPd@cuHRmdKI;Vst E0ObPp!vFvP literal 0 HcmV?d00001 diff --git a/example/src/styles.ts b/example/src/styles.ts index 8cb9eb3..bea55af 100644 --- a/example/src/styles.ts +++ b/example/src/styles.ts @@ -54,6 +54,11 @@ const styles = (params: StyleSheetParams = {}) => tintColor: Colors.white, alignSelf: 'flex-end', }, + stopButton: { + height: scale(22), + width: scale(22), + alignSelf: 'center', + }, pinkButtonImage: { height: scale(22), width: scale(22), diff --git a/src/components/Waveform/Waveform.tsx b/src/components/Waveform/Waveform.tsx index 7934eea..208413f 100644 --- a/src/components/Waveform/Waveform.tsx +++ b/src/components/Waveform/Waveform.tsx @@ -67,6 +67,7 @@ export const Waveform = forwardRef((props, ref) => { const viewRef = useRef(null); const scrollRef = useRef(null); const isLayoutCalculated = useRef(false); + const isAutoPaused = useRef(false); const [waveform, setWaveform] = useState([]); const [viewLayout, setViewLayout] = useState(null); const [seekPosition, setSeekPosition] = useState( @@ -518,11 +519,14 @@ export const Waveform = forwardRef((props, ref) => { if (panMoving) { if (playerState === PlayerState.playing) { pausePlayerAction(); + isAutoPaused.current = true; } } else { - if (playerState === PlayerState.paused) { + if (playerState === PlayerState.paused && isAutoPaused.current) { startPlayerAction(); } + + isAutoPaused.current = false; } // eslint-disable-next-line react-hooks/exhaustive-deps }, [panMoving]); @@ -559,6 +563,11 @@ export const Waveform = forwardRef((props, ref) => { (onPanStateChange as Function)(false); setPanMoving(false); }, + onPanResponderRelease: e => { + setSeekPosition(e.nativeEvent); + (onPanStateChange as Function)(false); + setPanMoving(false); + }, }) ).current; From 62ff0af067b04e7fa7af30897ae2edea0675150e Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 19 Oct 2024 22:54:09 -0400 Subject: [PATCH 12/20] fix: stop only the current player and not all players fix: stop other player instead of the recoding when starting recording --- example/src/App.tsx | 66 ++++++++++++------------ src/components/Waveform/Waveform.tsx | 2 + src/components/Waveform/WaveformTypes.ts | 2 + 3 files changed, 36 insertions(+), 34 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 63a7b98..43af71a 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -44,18 +44,15 @@ import { Colors } from './theme'; import FastImage from 'react-native-fast-image'; import fs from 'react-native-fs'; +let currentPlayingRef: React.RefObject | null = null; const RenderListItem = React.memo( ({ item, - currentPlaying, - setCurrentPlaying, onPanStateChange, currentPlaybackSpeed, changeSpeed, }: { item: ListItem; - currentPlaying: string; - setCurrentPlaying: Dispatch>; onPanStateChange: (value: boolean) => void; currentPlaybackSpeed: PlaybackSpeedType; changeSpeed: () => void; @@ -65,35 +62,36 @@ const RenderListItem = React.memo( const styles = stylesheet({ currentUser: item.fromCurrentUser }); const [isLoading, setIsLoading] = useState(true); - const handleButtonAction = () => { - if (playerState === PlayerState.stopped) { - setCurrentPlaying(item.path); - } else if (playerState === PlayerState.playing) { - ref.current?.pausePlayer(); - } else { - ref.current?.resumePlayer(); + const handlePlayStopAction = async () => { + if (currentPlayingRef == null) { + currentPlayingRef = ref; + await currentPlayingRef.current?.startPlayer({ + finishMode: FinishMode.stop, + }); + } else if ( + currentPlayingRef.current?.currentState === PlayerState.playing + ) { + await currentPlayingRef?.current?.stopPlayer(); + if ( + currentPlayingRef.current.playerKey() !== ref?.current?.playerKey() + ) { + currentPlayingRef = ref; + await currentPlayingRef.current?.startPlayer({ + finishMode: FinishMode.stop, + }); + } else { + currentPlayingRef = null; + } } }; - const handleStopAction = () => { - setCurrentPlaying(''); - }; - - useEffect(() => { - if (currentPlaying !== item.path) { - ref.current?.stopPlayer(); - } else { - ref.current?.startPlayer({ finishMode: FinishMode.stop }); - } - }, [currentPlaying]); - return ( {isLoading ? ( @@ -142,12 +140,7 @@ const RenderListItem = React.memo( candleHeightScale={4} onPlayerStateChange={state => { setPlayerState(state); - if ( - state === PlayerState.stopped && - currentPlaying === item.path - ) { - setCurrentPlaying(''); - } + // console.log(`state changed ${state}`); }} onPanStateChange={onPanStateChange} onError={error => { @@ -203,13 +196,20 @@ const LivePlayerComponent = ({ const handleRecorderAction = async () => { if (recorderState === RecorderState.stopped) { - let hasPermission = await checkHasAudioRecorderPermission(); + // Stopping other player before starting recording + if (currentPlayingRef?.current?.currentState === PlayerState.playing) { + currentPlayingRef?.current?.stopPlayer(); + } + + const hasPermission = await checkHasAudioRecorderPermission(); if (hasPermission === PermissionStatus.granted) { + currentPlayingRef = ref; startRecording(); } else if (hasPermission === PermissionStatus.undetermined) { const permissionStatus = await getAudioRecorderPermission(); if (permissionStatus === PermissionStatus.granted) { + currentPlayingRef = ref; startRecording(); } } else { @@ -219,6 +219,7 @@ const LivePlayerComponent = ({ ref.current?.stopRecord().then(path => { setList(prev => [...prev, { fromCurrentUser: true, path }]); }); + currentPlayingRef = null; } }; @@ -250,7 +251,6 @@ const LivePlayerComponent = ({ const AppContainer = () => { const [shouldScroll, setShouldScroll] = useState(true); - const [currentPlaying, setCurrentPlaying] = useState(''); const [list, setList] = useState([]); const [nbOfRecording, setNumberOfRecording] = useState(0); const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = @@ -350,8 +350,6 @@ const AppContainer = () => { {list.map(item => ( setShouldScroll(!value)} {...{ currentPlaybackSpeed, changeSpeed }} diff --git a/src/components/Waveform/Waveform.tsx b/src/components/Waveform/Waveform.tsx index 208413f..201a8e3 100644 --- a/src/components/Waveform/Waveform.tsx +++ b/src/components/Waveform/Waveform.tsx @@ -586,6 +586,8 @@ export const Waveform = forwardRef((props, ref) => { pauseRecord: pauseRecordingAction, stopRecord: stopRecordingAction, resumeRecord: resumeRecordingAction, + currentState: mode === 'static' ? playerState : recorderState, + playerKey: () => path, })); return ( diff --git a/src/components/Waveform/WaveformTypes.ts b/src/components/Waveform/WaveformTypes.ts index f3a5dd3..1ad67f9 100644 --- a/src/components/Waveform/WaveformTypes.ts +++ b/src/components/Waveform/WaveformTypes.ts @@ -51,4 +51,6 @@ export interface IWaveformRef { stopRecord: () => Promise; pauseRecord: () => Promise; resumeRecord: () => Promise; + currentState: PlayerState | RecorderState; + playerKey: () => string; } From b3479cc14770748e67ddf8e8f2442f1faee346d4 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 9 Nov 2024 10:23:06 -0500 Subject: [PATCH 13/20] fix: playing was not restarting once stopped --- example/src/App.tsx | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 43af71a..f3eb592 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -63,24 +63,31 @@ const RenderListItem = React.memo( const [isLoading, setIsLoading] = useState(true); const handlePlayStopAction = async () => { - if (currentPlayingRef == null) { + console.log('currentPlayingRef', currentPlayingRef); + + let currentPlayer = currentPlayingRef?.current; + + // If no player or if current player is stopped just start it! + if ( + currentPlayer == null || + currentPlayer.currentState === PlayerState.stopped + ) { currentPlayingRef = ref; await currentPlayingRef.current?.startPlayer({ finishMode: FinishMode.stop, }); - } else if ( - currentPlayingRef.current?.currentState === PlayerState.playing - ) { - await currentPlayingRef?.current?.stopPlayer(); - if ( - currentPlayingRef.current.playerKey() !== ref?.current?.playerKey() - ) { + } else { + // Always stop current player if it was playing + if (currentPlayer.currentState === PlayerState.playing) { + await currentPlayingRef?.current?.stopPlayer(); + } + + // Start player only when it is a different one! + if (currentPlayer.playerKey() !== ref?.current?.playerKey()) { currentPlayingRef = ref; await currentPlayingRef.current?.startPlayer({ finishMode: FinishMode.stop, }); - } else { - currentPlayingRef = null; } } }; @@ -140,19 +147,19 @@ const RenderListItem = React.memo( candleHeightScale={4} onPlayerStateChange={state => { setPlayerState(state); - // console.log(`state changed ${state}`); + console.log(`state changed ${state}`); }} onPanStateChange={onPanStateChange} onError={error => { console.log(error, 'we are in example'); }} onCurrentProgressChange={(currentProgress, songDuration) => { - console.log( - 'currentProgress ', - currentProgress, - 'songDuration ', - songDuration - ); + // console.log( + // 'currentProgress ', + // currentProgress, + // 'songDuration ', + // songDuration + // ); }} onChangeWaveformLoadState={state => { setIsLoading(state); From 6cdc6b22b2236e9540f8fde63acf9c2cb0e4f2d6 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 9 Nov 2024 10:31:16 -0500 Subject: [PATCH 14/20] chore: code review --- example/src/App.tsx | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index f3eb592..ba2bb0d 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -44,7 +44,7 @@ import { Colors } from './theme'; import FastImage from 'react-native-fast-image'; import fs from 'react-native-fs'; -let currentPlayingRef: React.RefObject | null = null; +let currentPlayingRef: React.RefObject | undefined; const RenderListItem = React.memo( ({ item, @@ -63,8 +63,6 @@ const RenderListItem = React.memo( const [isLoading, setIsLoading] = useState(true); const handlePlayStopAction = async () => { - console.log('currentPlayingRef', currentPlayingRef); - let currentPlayer = currentPlayingRef?.current; // If no player or if current player is stopped just start it! @@ -82,7 +80,7 @@ const RenderListItem = React.memo( await currentPlayingRef?.current?.stopPlayer(); } - // Start player only when it is a different one! + // Start player when it is a different one! if (currentPlayer.playerKey() !== ref?.current?.playerKey()) { currentPlayingRef = ref; await currentPlayingRef.current?.startPlayer({ @@ -145,21 +143,15 @@ const RenderListItem = React.memo( scrubColor={Colors.white} waveColor={Colors.lightWhite} candleHeightScale={4} - onPlayerStateChange={state => { - setPlayerState(state); - console.log(`state changed ${state}`); - }} + onPlayerStateChange={setPlayerState} onPanStateChange={onPanStateChange} onError={error => { console.log(error, 'we are in example'); }} onCurrentProgressChange={(currentProgress, songDuration) => { - // console.log( - // 'currentProgress ', - // currentProgress, - // 'songDuration ', - // songDuration - // ); + console.log( + `currentProgress ${currentProgress}, songDuration ${songDuration}` + ); }} onChangeWaveformLoadState={state => { setIsLoading(state); @@ -226,7 +218,7 @@ const LivePlayerComponent = ({ ref.current?.stopRecord().then(path => { setList(prev => [...prev, { fromCurrentUser: true, path }]); }); - currentPlayingRef = null; + currentPlayingRef = undefined; } }; From b9b275311aa2ee13abcab5666f80ae5fc838583c Mon Sep 17 00:00:00 2001 From: David Prevost Date: Sat, 9 Nov 2024 10:38:58 -0500 Subject: [PATCH 15/20] fix: do not start player when recording --- example/src/App.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/example/src/App.tsx b/example/src/App.tsx index ba2bb0d..727cc86 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -75,6 +75,11 @@ const RenderListItem = React.memo( finishMode: FinishMode.stop, }); } else { + // If we are recording do nothing + if (currentPlayer.currentState === RecorderState.recording) { + return; + } + // Always stop current player if it was playing if (currentPlayer.currentState === PlayerState.playing) { await currentPlayingRef?.current?.stopPlayer(); From 686e71f5a6b481c8c4f5b6ed6bfee64bd8c87d4c Mon Sep 17 00:00:00 2001 From: David Prevost Date: Thu, 14 Nov 2024 19:35:36 -0500 Subject: [PATCH 16/20] fix: reconcile code with the new stop and pause button --- example/src/App.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 727cc86..8597189 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -62,7 +62,7 @@ const RenderListItem = React.memo( const styles = stylesheet({ currentUser: item.fromCurrentUser }); const [isLoading, setIsLoading] = useState(true); - const handlePlayStopAction = async () => { + const handlePlayPauseAction = async () => { let currentPlayer = currentPlayingRef?.current; // If no player or if current player is stopped just start it! @@ -80,9 +80,9 @@ const RenderListItem = React.memo( return; } - // Always stop current player if it was playing + // Pause current player if it was playing if (currentPlayer.currentState === PlayerState.playing) { - await currentPlayingRef?.current?.stopPlayer(); + await currentPlayingRef?.current?.pausePlayer(); } // Start player when it is a different one! @@ -95,13 +95,17 @@ const RenderListItem = React.memo( } }; + const handleStopAction = async () => { + ref.current?.stopPlayer(); + }; + return ( {isLoading ? ( From f1f0205aa3d649401de540562b8d9ec321193406 Mon Sep 17 00:00:00 2001 From: nilesh-simform Date: Fri, 15 Nov 2024 13:59:29 +0530 Subject: [PATCH 17/20] fix(UNT-T30609): kotlin issue while building android --- android/src/main/java/com/audiowaveform/AudioWaveformModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt b/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt index 6565958..698ca5c 100644 --- a/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt +++ b/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt @@ -316,7 +316,7 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav } } override fun onReject(error: String?, message: String?) { - promise.reject(error, message) + promise.reject(error ?: "Error", message ?: "An error is thrown while decoding the audio file") } override fun onResolve(value: MutableList>) { promise.resolve(Arguments.fromList(value)) From 2f0b5769c218f8ceed8c2bf53573794b563171c3 Mon Sep 17 00:00:00 2001 From: David Prevost Date: Fri, 15 Nov 2024 07:53:23 -0500 Subject: [PATCH 18/20] fix: reconcile code with the pause/resume feature --- example/src/App.tsx | 43 ++++++++++++++---------- src/components/Waveform/Waveform.tsx | 2 +- src/components/Waveform/WaveformTypes.ts | 2 +- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/example/src/App.tsx b/example/src/App.tsx index 8597189..7e7c666 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -63,34 +63,41 @@ const RenderListItem = React.memo( const [isLoading, setIsLoading] = useState(true); const handlePlayPauseAction = async () => { - let currentPlayer = currentPlayingRef?.current; - - // If no player or if current player is stopped just start it! + // If we are recording do nothing if ( - currentPlayer == null || - currentPlayer.currentState === PlayerState.stopped + currentPlayingRef?.current?.currentState === RecorderState.recording ) { + return; + } + + const startNewPlayer = async () => { currentPlayingRef = ref; - await currentPlayingRef.current?.startPlayer({ - finishMode: FinishMode.stop, - }); - } else { - // If we are recording do nothing - if (currentPlayer.currentState === RecorderState.recording) { - return; + if (ref.current?.currentState === PlayerState.paused) { + await ref.current?.resumePlayer(); + } else { + await ref.current?.startPlayer({ + finishMode: FinishMode.stop, + }); } + }; + // If no player or if current player is stopped just start the new player! + if ( + currentPlayingRef == null || + [PlayerState.stopped, PlayerState.paused].includes( + currentPlayingRef?.current?.currentState as PlayerState + ) + ) { + await startNewPlayer(); + } else { // Pause current player if it was playing - if (currentPlayer.currentState === PlayerState.playing) { + if (currentPlayingRef?.current?.currentState === PlayerState.playing) { await currentPlayingRef?.current?.pausePlayer(); } // Start player when it is a different one! - if (currentPlayer.playerKey() !== ref?.current?.playerKey()) { - currentPlayingRef = ref; - await currentPlayingRef.current?.startPlayer({ - finishMode: FinishMode.stop, - }); + if (currentPlayingRef?.current?.playerKey !== ref?.current?.playerKey) { + await startNewPlayer(); } } }; diff --git a/src/components/Waveform/Waveform.tsx b/src/components/Waveform/Waveform.tsx index 201a8e3..d62154f 100644 --- a/src/components/Waveform/Waveform.tsx +++ b/src/components/Waveform/Waveform.tsx @@ -587,7 +587,7 @@ export const Waveform = forwardRef((props, ref) => { stopRecord: stopRecordingAction, resumeRecord: resumeRecordingAction, currentState: mode === 'static' ? playerState : recorderState, - playerKey: () => path, + playerKey: path, })); return ( diff --git a/src/components/Waveform/WaveformTypes.ts b/src/components/Waveform/WaveformTypes.ts index 1ad67f9..3d73c88 100644 --- a/src/components/Waveform/WaveformTypes.ts +++ b/src/components/Waveform/WaveformTypes.ts @@ -52,5 +52,5 @@ export interface IWaveformRef { pauseRecord: () => Promise; resumeRecord: () => Promise; currentState: PlayerState | RecorderState; - playerKey: () => string; + playerKey: string; } From 84bac61f3b8f5020f6d6231132663788142e24c9 Mon Sep 17 00:00:00 2001 From: prince_dobariya Date: Mon, 18 Nov 2024 16:34:24 +0530 Subject: [PATCH 19/20] chor: update the lib version to 2.1.2 --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5d9eaee..444524f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@simform_solutions/react-native-audio-waveform", - "version": "2.1.1", + "version": "2.1.2", "description": "A React Native component to show audio waveform with ease in react native application", "main": "lib/index", "types": "lib/index.d.ts", @@ -112,4 +112,4 @@ "dependencies": { "lodash": "^4.17.21" } -} \ No newline at end of file +} From 8b19f08df4d4bbd1ad1645b02d4f2a06fe1b897f Mon Sep 17 00:00:00 2001 From: nilesh-simform Date: Tue, 19 Nov 2024 11:20:41 +0530 Subject: [PATCH 20/20] fix(UNT-T30665): promises leak android --- .../com/audiowaveform/AudioWaveformModule.kt | 58 +++++++++---------- src/components/Waveform/Waveform.tsx | 43 ++++++++++---- 2 files changed, 61 insertions(+), 40 deletions(-) diff --git a/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt b/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt index 698ca5c..47a0036 100644 --- a/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt +++ b/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt @@ -159,13 +159,10 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav @ReactMethod fun startPlayer(obj: ReadableMap, promise: Promise) { val finishMode = obj.getInt(Constants.finishMode) - val key = obj.getString(Constants.playerKey) val speed = obj.getDouble(Constants.speed) - if (key != null) { - audioPlayers[key]?.start(finishMode ?: 2, speed.toFloat(),promise) - } else { - promise.reject("startPlayer Error", "Player key can't be null") - } + + val player = getPlayerOrReject(obj, promise, "startPlayer Error"); + player?.start(finishMode ?: 2, speed.toFloat(),promise) } @ReactMethod @@ -182,12 +179,8 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav @ReactMethod fun pausePlayer(obj: ReadableMap, promise: Promise) { - val key = obj.getString(Constants.playerKey) - if (key != null) { - audioPlayers[key]?.pause(promise) - } else { - promise.reject("pausePlayer Error", "Player key can't be null") - } + val player = getPlayerOrReject(obj, promise, "pausePlayer Error"); + player?.pause(promise); } @ReactMethod @@ -195,12 +188,9 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val progress = obj.getInt(Constants.progress) - val key = obj.getString(Constants.playerKey) - if (key != null) { - audioPlayers[key]?.seekToPosition(progress.toLong(), promise) - } else { - promise.reject("seekTo Error", "Player key can't be null") - } + + val player = getPlayerOrReject(obj, promise, "seekTo Error"); + player?.seekToPosition(progress.toLong(), promise) } else { Log.e( Constants.LOG_TAG, @@ -216,24 +206,18 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav @ReactMethod fun setVolume(obj: ReadableMap, promise: Promise) { val volume = obj.getInt(Constants.volume) - val key = obj.getString(Constants.playerKey) - if (key != null) { - audioPlayers[key]?.setVolume(volume.toFloat(), promise) - } else { - promise.reject("setVolume error", "Player key can't be null") - } + + val player = getPlayerOrReject(obj, promise, "setVolume Error"); + player?.setVolume(volume.toFloat(), promise) } @ReactMethod fun getDuration(obj: ReadableMap, promise: Promise) { - val key = obj.getString(Constants.playerKey) val duration = obj.getInt(Constants.durationType) val type = if (duration == 0) DurationType.Current else DurationType.Max - if (key != null) { - audioPlayers[key]?.getDuration(type, promise) - } else { - promise.reject("getDuration Error", "Player key can't be null") - } + + val player = getPlayerOrReject(obj, promise, "getDuration Error"); + player?.getDuration(type, promise) } @ReactMethod @@ -429,4 +413,18 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav handler.removeCallbacks(emitLiveRecordValue) } + private fun getPlayerOrReject(arguments: ReadableMap, promise: Promise, errorCode: String): AudioPlayer? { + val key = getPlayerKeyOrReject(arguments, promise, errorCode) + return audioPlayers[key] ?: run { + promise.reject(errorCode, "$errorCode: Player not in the list") + null + } + } + + private fun getPlayerKeyOrReject(arguments: ReadableMap, promise: Promise, errorCode: String): String? { + return arguments.getString(Constants.playerKey) ?: run { + promise.reject(errorCode, "$errorCode: Player key can't be null") + null + } + } } \ No newline at end of file diff --git a/src/components/Waveform/Waveform.tsx b/src/components/Waveform/Waveform.tsx index d62154f..0961a15 100644 --- a/src/components/Waveform/Waveform.tsx +++ b/src/components/Waveform/Waveform.tsx @@ -209,14 +209,17 @@ export const Waveform = forwardRef((props, ref) => { } }; - const stopPlayerAction = async () => { + const stopPlayerAction = async (resetProgress = true) => { if (mode === 'static') { try { const result = await stopPlayer({ playerKey: `PlayerFor${path}`, }); if (!isNil(result) && result) { - setCurrentProgress(0); + if (resetProgress) { + setCurrentProgress(0); + } + setPlayerState(PlayerState.stopped); return Promise.resolve(result); } else { @@ -258,6 +261,11 @@ export const Waveform = forwardRef((props, ref) => { ); } } catch (error) { + if (playerState === PlayerState.paused) { + // If the player is not prepared, triggering the stop will reset the player for next click. Fix blocked paused player after a call to `stopAllPlayers` + await stopPlayerAction(); + } + return Promise.reject(error); } } else { @@ -267,14 +275,17 @@ export const Waveform = forwardRef((props, ref) => { } }; - const pausePlayerAction = async () => { + const pausePlayerAction = async (changePlayerState = true) => { if (mode === 'static') { try { const pause = await pausePlayer({ playerKey: `PlayerFor${path}`, }); if (pause) { - setPlayerState(PlayerState.paused); + if (changePlayerState) { + setPlayerState(PlayerState.paused); + } + return Promise.resolve(true); } else { return Promise.reject( @@ -423,7 +434,7 @@ export const Waveform = forwardRef((props, ref) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [viewLayout?.width, mode, candleWidth, candleSpace]); - useEffect(() => { + const seekToPlayerAction = async () => { if (!isNil(seekPosition)) { if (mode === 'static') { const seekAmount = @@ -432,10 +443,18 @@ export const Waveform = forwardRef((props, ref) => { const clampedSeekAmount = clamp(seekAmount, 0, 1); if (!panMoving) { - seekToPlayer({ - playerKey: `PlayerFor${path}`, - progress: clampedSeekAmount * songDuration, - }); + try { + await seekToPlayer({ + playerKey: `PlayerFor${path}`, + progress: clampedSeekAmount * songDuration, + }); + } catch (e) { + if (playerState === PlayerState.paused) { + // If the player is not prepared, triggering the stop will reset the player for next click. Fix blocked paused player after a call to `stopAllPlayers` + await stopPlayerAction(false); + } + } + if (playerState === PlayerState.playing) { startPlayerAction(); } @@ -444,6 +463,10 @@ export const Waveform = forwardRef((props, ref) => { setCurrentProgress(clampedSeekAmount * songDuration); } } + }; + + useEffect(() => { + seekToPlayerAction(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [seekPosition, panMoving, mode, songDuration]); @@ -518,7 +541,7 @@ export const Waveform = forwardRef((props, ref) => { useEffect(() => { if (panMoving) { if (playerState === PlayerState.playing) { - pausePlayerAction(); + pausePlayerAction(false); isAutoPaused.current = true; } } else {