diff --git a/README.md b/README.md index bf98167..690961e 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,33 @@ resumePlayer(): Promise It returns a boolean indicating whether playback is resumed again. +#### stopAllPlayers() + +```ts +stopAllPlayers(): Promise +``` + +Stops all the players at once and frees their native resources. Useful on unmount! +It returns a boolean indicating that all players were stopped. + +#### stopAllWaveFormExtractors() + +```ts +stopAllWaveFormExtractors(): Promise +``` + +Stops all the extractors used to build the audio waveform and frees its native resource. Useful on unmount! +It returns a boolean indicating that all extractors were stopped. + +#### stopPlayersAndExtractors() + +```ts +stopPlayersAndExtractors(): Promise<[boolean, boolean]> +``` + +Combined the `stopAllWaveFormExtractors` and `stopAllPlayers` in one call to free up the maximum possible resources. Very useful on unmount! +It returns an array of two booleans indicating if all players and all waveform extractors were stopped. + #### For Live mode #### startRecord() diff --git a/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt b/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt index 47a0036..1266638 100644 --- a/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt +++ b/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt @@ -17,6 +17,7 @@ import com.facebook.react.modules.core.DeviceEventManagerModule import java.io.File import java.io.IOException import java.text.SimpleDateFormat +import java.util.Collections import java.util.Date import java.util.Locale @@ -234,11 +235,28 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav @ReactMethod fun stopAllPlayers(promise: Promise) { - for ((key, _) in audioPlayers) { - audioPlayers[key]?.stop() - audioPlayers[key] = null + try { + audioPlayers.values.forEach{ + player -> player?.stop() + } + audioPlayers.clear() + promise.resolve(true) + } catch (err: Exception) { + promise.reject("stopAllPlayers Error", "Error while stopping all players") + } + } + + @ReactMethod + fun stopAllWaveFormExtractors(promise: Promise) { + try { + extractors.values.forEach{ + extractor -> extractor?.forceStop() + } + extractors.clear() + promise.resolve(true) + } catch (err: Exception) { + promise.reject("stopAllExtractors Error", "Error while stopping all extractors") } - promise.resolve(true) } @ReactMethod @@ -305,6 +323,9 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav override fun onResolve(value: MutableList>) { promise.resolve(Arguments.fromList(value)) } + override fun onForceStop() { + promise.resolve(Arguments.fromList(mutableListOf(emptyList()))) + } } ) extractors[playerKey]?.startDecode(); diff --git a/android/src/main/java/com/audiowaveform/WaveformExtractor.kt b/android/src/main/java/com/audiowaveform/WaveformExtractor.kt index 1a866b9..b4dabf7 100644 --- a/android/src/main/java/com/audiowaveform/WaveformExtractor.kt +++ b/android/src/main/java/com/audiowaveform/WaveformExtractor.kt @@ -12,7 +12,6 @@ import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.WritableMap import com.facebook.react.modules.core.DeviceEventManagerModule import java.nio.ByteBuffer -import java.util.concurrent.CountDownLatch import kotlin.math.pow import kotlin.math.sqrt import java.io.File @@ -32,7 +31,6 @@ class WaveformExtractor( @Volatile private var inProgress = false - private val finishCount = CountDownLatch(1) private var inputEof = false private var sampleRate = 0 private var channels = 1 @@ -122,7 +120,6 @@ class WaveformExtractor( Constants.LOG_TAG + " " + e.message, "An error is thrown while decoding the audio file" ) - finishCount.countDown() } override fun onOutputBufferAvailable( @@ -256,13 +253,18 @@ class WaveformExtractor( } } + fun forceStop() { + stop() + // When stopped by outside we must notify to resolved the hanging promises + extractorCallBack.onForceStop() + } + private fun stop() { if (!inProgress) return inProgress = false decoder?.stop() decoder?.release() extractor?.release() - finishCount.countDown() } } @@ -272,4 +274,5 @@ interface ExtractorCallBack { fun onProgress(value: Float) fun onReject(error: String?, message: String?) fun onResolve(value: MutableList>) + fun onForceStop() } \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 11db637..be7fcd5 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -74,18 +74,6 @@ 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 @@ -387,7 +375,7 @@ PODS: - React-jsinspector (0.72.7) - React-logger (0.72.7): - glog - - react-native-audio-waveform (1.0.0): + - react-native-audio-waveform (2.1.2): - RCT-Folly (= 2021.07.22.00) - React-Core - react-native-safe-area-context (4.11.0): @@ -504,21 +492,11 @@ 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.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): @@ -590,7 +568,6 @@ 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`) @@ -608,10 +585,7 @@ SPEC REPOS: - FlipperKit - fmt - libevent - - libwebp - OpenSSL-Universal - - SDWebImage - - SDWebImageWebPCoder - SocketRocket - YogaKit @@ -699,8 +673,6 @@ 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: @@ -726,7 +698,6 @@ SPEC CHECKSUMS: glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b hermes-engine: 9180d43df05c1ed658a87cc733dc3044cf90c00a libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 - libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c RCT-Folly: 424b8c9a7a0b9ab2886ffe9c3b041ef628fd4fb1 RCTRequired: 83bca1c184feb4d2e51c72c8369b83d641443f95 @@ -743,7 +714,7 @@ SPEC CHECKSUMS: React-jsiexecutor: c49502e5d02112247ee4526bc3ccfc891ae3eb9b React-jsinspector: 8baadae51f01d867c3921213a25ab78ab4fbcd91 React-logger: 8edc785c47c8686c7962199a307015e2ce9a0e4f - react-native-audio-waveform: 7cdb6e4963eeae907240396975b9c79713591758 + react-native-audio-waveform: 99f401dee91ac357ce40cba147a31a18b539d312 react-native-safe-area-context: 851c62c48dce80ccaa5637b6aa5991a1bc36eca9 React-NativeModulesApple: b6868ee904013a7923128892ee4a032498a1024a React-perflogger: 31ea61077185eb1428baf60c0db6e2886f141a5a @@ -763,11 +734,8 @@ SPEC CHECKSUMS: React-utils: 56838edeaaf651220d1e53cd0b8934fb8ce68415 ReactCommon: 5f704096ccf7733b390f59043b6fa9cc180ee4f6 rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba - RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: 7ad14a6c7b491add489246611d324f10009083ac - SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d - SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Yoga: 4c3aa327e4a6a23eeacd71f61c81df1bcdf677d5 YogaKit: f782866e155069a2cca2517aafea43200b01fd5a diff --git a/example/package.json b/example/package.json index 48c932f..3aff472 100644 --- a/example/package.json +++ b/example/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "android": "react-native run-android", + "ios:pods": "bundle exec pod install --project-directory=ios", "ios": "react-native run-ios", "lint": "eslint .", "start": "react-native start", @@ -12,7 +13,6 @@ "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 7e7c666..252bb88 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -8,6 +8,7 @@ import { UpdateFrequency, Waveform, useAudioPermission, + useAudioPlayer, } from '@simform_solutions/react-native-audio-waveform'; import React, { Dispatch, @@ -41,7 +42,6 @@ import { } from './constants'; import stylesheet from './styles'; import { Colors } from './theme'; -import FastImage from 'react-native-fast-image'; import fs from 'react-native-fs'; let currentPlayingRef: React.RefObject | undefined; @@ -78,6 +78,13 @@ const RenderListItem = React.memo( await ref.current?.startPlayer({ finishMode: FinishMode.stop, }); + + // If the player took too much time to initialize and another player started instead we pause the former one! + if ( + currentPlayingRef?.current?.playerKey !== ref?.current?.playerKey + ) { + await ref?.current?.pausePlayer(); + } } }; @@ -117,7 +124,7 @@ const RenderListItem = React.memo( {isLoading ? ( ) : ( - - {isLoading ? ( - - ) : ( - - )} + { - console.log(error, 'we are in example'); + console.log('Error in static player:', error); }} - onCurrentProgressChange={(currentProgress, songDuration) => { - console.log( - `currentProgress ${currentProgress}, songDuration ${songDuration}` - ); + onCurrentProgressChange={(_currentProgress, _songDuration) => { + // console.log( + // `currentProgress ${currentProgress}, songDuration ${songDuration}` + // ); }} onChangeWaveformLoadState={state => { setIsLoading(state); @@ -270,6 +273,8 @@ const AppContainer = () => { const [nbOfRecording, setNumberOfRecording] = useState(0); const [currentPlaybackSpeed, setCurrentPlaybackSpeed] = useState(1.0); + const [showAdvancedOptions, setShowAdvancedOptions] = + useState(false); const { top, bottom } = useSafeAreaInsets(); const styles = stylesheet({ top, bottom }); @@ -327,6 +332,31 @@ const AppContainer = () => { ); }; + const toggleAdvancedOptions = () => { + setShowAdvancedOptions(!showAdvancedOptions); + }; + + const handleStopPlayersAndExtractors = async () => { + await currentPlayingRef?.current?.stopPlayer(); + + const { stopPlayersAndExtractors } = useAudioPlayer(); + const hasStoppedAll: boolean[] = await stopPlayersAndExtractors(); + + if (hasStoppedAll.every(Boolean)) { + Alert.alert( + 'Everything stopped', + 'All players and extractors have been stopped!', + [{ text: 'OK' }] + ); + } else { + Alert.alert( + 'Error stopping everything', + 'An error occurred when trying to stop players or extractors', + [{ text: 'OK' }] + ); + } + }; + return ( { - + - - - - {'Delete recorded audio files'} - - - + + {showAdvancedOptions && ( + + + + + {'Delete recorded audio files'} + + + + + + {'Stop all players and extractors'} + + + + )} {list.map(item => ( => { ); // Filter out unsuccessful file copies - return successfulCopies?.filter?.(value => value !== null); + return successfulCopies?.filter?.((value): value is string => value !== null); } // On iOS, return all files without copying diff --git a/example/src/styles.ts b/example/src/styles.ts index bea55af..5e02853 100644 --- a/example/src/styles.ts +++ b/example/src/styles.ts @@ -101,22 +101,22 @@ const styles = (params: StyleSheetParams = {}) => width: '100%', tintColor: Colors.pink, }, - headerContainer: { + simformImageContainer: { alignItems: 'center', }, - deleteRecordingContainer: { + advancedOptionsContainer: { + gap: scale(8), + }, + advancedOptionItem: { alignItems: 'center', flexDirection: 'row', }, - deleteRecordingTitle: { + advancedOptionItemTitle: { fontSize: scale(20), fontWeight: 'bold', color: Colors.pink, paddingLeft: scale(8), }, - loadingText: { - color: Colors.black, - }, speedBox: { height: scale(28), width: scale(28), diff --git a/ios/AudioWaveform.m b/ios/AudioWaveform.m index 241a4c4..06109e4 100644 --- a/ios/AudioWaveform.m +++ b/ios/AudioWaveform.m @@ -64,4 +64,6 @@ @interface RCT_EXTERN_MODULE(AudioWaveform, RCTEventEmitter) resolver: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(markPlayerAsUnmounted) +RCT_EXTERN_METHOD(stopAllWaveFormExtractors:(RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject) @end diff --git a/ios/AudioWaveform.swift b/ios/AudioWaveform.swift index 00b42f2..409a31c 100644 --- a/ios/AudioWaveform.swift +++ b/ios/AudioWaveform.swift @@ -231,8 +231,16 @@ class AudioWaveform: RCTEventEmitter { @objc func stopAllPlayers(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void { for (playerKey,_) in audioPlayers{ audioPlayers[playerKey]?.stopPlayer() - audioPlayers[playerKey] = nil } + audioPlayers.removeAll() + resolve(true) + } + + @objc func stopAllWaveFormExtractors(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) -> Void { + for (extractorKey,_) in extractors{ + extractors[extractorKey]?.cancel() + } + extractors.removeAll() resolve(true) } diff --git a/ios/Utils.swift b/ios/Utils.swift index 503e124..9a10019 100644 --- a/ios/Utils.swift +++ b/ios/Utils.swift @@ -58,6 +58,7 @@ struct Constants { static let currentDecibel = "currentDecibel" static let playerKey = "playerKey" static let stopAllPlayers = "stopAllPlayers" + static let stopAllWaveFormExtractors = "stopAllWaveFormExtractors" static let onDidFinishPlayingAudio = "onDidFinishPlayingAudio" static let finishMode = "finishMode" static let speed = "speed" diff --git a/src/components/Waveform/Waveform.tsx b/src/components/Waveform/Waveform.tsx index 80fd160..2164731 100644 --- a/src/components/Waveform/Waveform.tsx +++ b/src/components/Waveform/Waveform.tsx @@ -59,10 +59,10 @@ export const Waveform = forwardRef((props, ref) => { onPlayerStateChange, onRecorderStateChange, onPanStateChange = () => {}, - onError = () => {}, + onError = (_error: Error) => {}, onCurrentProgressChange = () => {}, candleHeightScale = 3, - onChangeWaveformLoadState, + onChangeWaveformLoadState = (_state: boolean) => {}, } = props as StaticWaveform & LiveWaveform; const viewRef = useRef(null); const scrollRef = useRef(null); @@ -80,6 +80,7 @@ export const Waveform = forwardRef((props, ref) => { const [panMoving, setPanMoving] = useState(false); const [playerState, setPlayerState] = useState(PlayerState.stopped); const [recorderState, setRecorderState] = useState(RecorderState.stopped); + const [isWaveformExtracted, setWaveformExtracted] = useState(false); const audioSpeed: number = playbackSpeed > playbackSpeedThreshold ? 1.0 : playbackSpeed; @@ -174,39 +175,37 @@ export const Waveform = forwardRef((props, ref) => { } } } catch (err) { - console.error(err); - (onError as Function)(err); + onError(err as Error); } }; const getAudioWaveFormForPath = async (noOfSample: number) => { if (!isNil(path) && !isEmpty(path)) { try { - (onChangeWaveformLoadState as Function)?.(true); + onChangeWaveformLoadState(true); const result = await extractWaveformData({ path: path, playerKey: `PlayerFor${path}`, noOfSamples: Math.max(noOfSample, 1), }); - (onChangeWaveformLoadState as Function)?.(false); + onChangeWaveformLoadState(false); if (!isNil(result) && !isEmpty(result)) { const waveforms = head(result); if (!isNil(waveforms) && !isEmpty(waveforms)) { setWaveform(waveforms); await preparePlayerAndGetDuration(); + setWaveformExtracted(true); } } } catch (err) { - (onError as Function)(err); - (onChangeWaveformLoadState as Function)?.(false); - console.error(err); + onChangeWaveformLoadState(false); + onError(err as Error); } } else { - (onError as Function)( - `Can not find waveform for mode ${mode} path: ${path}` + onError( + new Error(`Can not find waveform for mode ${mode} path: ${path}`) ); - console.error(`Can not find waveform for mode ${mode} path: ${path}`); } }; @@ -244,7 +243,11 @@ export const Waveform = forwardRef((props, ref) => { try { isAudioPlaying.current = true; if (playerState === PlayerState.stopped) { - await preparePlayerForPath(currentProgress); + if (isWaveformExtracted) { + await preparePlayerForPath(currentProgress); + } else { + await getAudioWaveFormForPath(noOfSamples); + } } const play = await playPlayer({ diff --git a/src/components/Waveform/WaveformTypes.ts b/src/components/Waveform/WaveformTypes.ts index 3d73c88..ff8ab36 100644 --- a/src/components/Waveform/WaveformTypes.ts +++ b/src/components/Waveform/WaveformTypes.ts @@ -22,7 +22,7 @@ export interface StaticWaveform extends BaseWaveform { scrubColor?: string; onPlayerStateChange?: (playerState: PlayerState) => void; onPanStateChange?: (panMoving: boolean) => void; - onError?: (error: string) => void; + onError?: (error: Error) => void; onCurrentProgressChange?: ( currentProgress: number, songDuration: number diff --git a/src/hooks/useAudioPlayer.tsx b/src/hooks/useAudioPlayer.tsx index 03384e3..c231c65 100644 --- a/src/hooks/useAudioPlayer.tsx +++ b/src/hooks/useAudioPlayer.tsx @@ -40,6 +40,12 @@ export const useAudioPlayer = () => { const stopAllPlayers = () => AudioWaveform.stopAllPlayers(); + const stopAllWaveFormExtractors = () => + AudioWaveform.stopAllWaveFormExtractors(); + + const stopPlayersAndExtractors = () => + Promise.all([stopAllPlayers(), stopAllWaveFormExtractors()]); + const getDuration = (args: IGetDuration) => AudioWaveform.getDuration(args); const onDidFinishPlayingAudio = ( @@ -96,5 +102,7 @@ export const useAudioPlayer = () => { onCurrentRecordingWaveformData, setPlaybackSpeed, markPlayerAsUnmounted, + stopAllWaveFormExtractors, + stopPlayersAndExtractors, }; }; diff --git a/src/types/AudioWaveformTypes.ts b/src/types/AudioWaveformTypes.ts index a83d393..84ed169 100644 --- a/src/types/AudioWaveformTypes.ts +++ b/src/types/AudioWaveformTypes.ts @@ -202,6 +202,12 @@ export interface IAudioWaveforms extends NativeModule { */ stopAllPlayers(): Promise; + /** + * Stops all active waveform extractors. + * @returns A promise that resolves to a boolean indicating if all extractors were stopped successfully. + */ + stopAllWaveFormExtractors(): Promise; + /** * Sets the playback speed of the audio. * @param args - The playback speed to set, where 1.0 is normal speed.