diff --git a/README.md b/README.md index 76bf1a5..964583a 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,9 @@ const ref = useRef(null); candleSpace={2} candleWidth={4} onRecorderStateChange={recorderState => console.log(recorderState)} + onRecordingProgressChange={currentProgress => { + console.log(`currentProgress ${currentProgress}`); + }} />; ``` @@ -145,6 +148,7 @@ You can check out the full example at [Example](./example/src/App.tsx). | onRecorderStateChange | - | ❌ | ✅ | ( recorderState : RecorderState ) => void | callback function which returns the recorder state whenever the recorder state changes. Check RecorderState for more details | | onCurrentProgressChange | - | ✅ | ❌ | ( currentProgress : number, songDuration: number ) => void | callback function, which returns current progress of audio and total song duration. | | onChangeWaveformLoadState | - | ✅ | ❌ | ( state : boolean ) => void | callback function which returns the loading state of waveform candlestick. | +| onRecordingProgressChange | - | ❌ | ✅ | ( currentProgress : number ) => void | callback function which returns current progress of recording audio. | | onError | - | ✅ | ❌ | ( error : Error ) => void | callback function which returns the error for static audio waveform | | showsHorizontalScrollIndicator | false | ❌ | ✅ | boolean | whether to show scroll indicator when live waveform is being recorded and total width is more than parent view width | diff --git a/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt b/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt index 1266638..2976ec2 100644 --- a/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt +++ b/android/src/main/java/com/audiowaveform/AudioWaveformModule.kt @@ -17,7 +17,6 @@ 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 @@ -33,6 +32,8 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav private var bitRate: Int = 128000 private val handler = Handler(Looper.getMainLooper()) private var startTime: Long = 0 + private var emittingRecorderValueLastTime: Long = 0 + private var totalRecodingTime: Long = 0 companion object { const val NAME = "AudioWaveform" @@ -83,6 +84,7 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav val useLegacyNormalization = true audioRecorder.startRecorder(recorder, useLegacyNormalization, promise) startTime = System.currentTimeMillis() // Initialize startTime + totalRecodingTime = 0 startEmittingRecorderValue() } @@ -108,13 +110,13 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav } try { + stopEmittingRecorderValue() val currentTime = System.currentTimeMillis() if (currentTime - startTime < 500) { promise.reject("SHORT_RECORDING", "Recording is too short") return } - stopEmittingRecorderValue() audioRecorder.stopRecording(recorder, path!!, promise) recorder = null path = null @@ -414,6 +416,10 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav override fun run() { val currentDecibel = getDecibel() val args: WritableMap = Arguments.createMap() + val currentTime = System.currentTimeMillis() + val timeFromLastEmitting = currentTime - emittingRecorderValueLastTime + totalRecodingTime += timeFromLastEmitting + args.putInt(Constants.progress, totalRecodingTime.toInt()) if (currentDecibel == Double.NEGATIVE_INFINITY) { args.putDouble(Constants.currentDecibel, 0.0) } else { @@ -421,12 +427,14 @@ class AudioWaveformModule(context: ReactApplicationContext): ReactContextBaseJav args.putDouble(Constants.currentDecibel, currentDecibel/1000) } } + emittingRecorderValueLastTime = currentTime handler.postDelayed(this, UpdateFrequency.Low.value) reactApplicationContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)?.emit(Constants.onCurrentRecordingWaveformData, args) } } private fun startEmittingRecorderValue() { + emittingRecorderValueLastTime = System.currentTimeMillis() handler.postDelayed(emitLiveRecordValue, UpdateFrequency.Low.value) } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 765a3e6..cb23ff0 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1222,7 +1222,7 @@ PODS: - React-jsiexecutor - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - - react-native-audio-waveform (2.1.3): + - react-native-audio-waveform (2.1.4): - DoubleConversion - glog - hermes-engine @@ -1606,7 +1606,7 @@ PODS: - SDWebImageWebPCoder (~> 0.8.4) - RNFS (2.20.0): - React-Core - - RNGestureHandler (2.23.1): + - RNGestureHandler (2.24.0): - DoubleConversion - glog - hermes-engine @@ -1875,70 +1875,70 @@ SPEC CHECKSUMS: glog: eb93e2f488219332457c3c4eafd2738ddc7e80b8 hermes-engine: 1f783c3d53940aed0d2c84586f0b7a85ab7827ef libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 - RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809 + RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82 RCTDeprecation: f5c19ebdb8804b53ed029123eb69914356192fc8 RCTRequired: 6ae6cebe470486e0e0ce89c1c0eabb998e7c51f4 RCTTypeSafety: 50d6ec72a3d13cf77e041ff43a0617050fb98e3f React: e46fdbd82d2de942970c106677056f3bdd438d82 React-callinvoker: b027ad895934b5f27ce166d095ed0d272d7df619 - React-Core: 36b7f20f655d47a35046e2b02c9aa5a8f1bcb61e - React-CoreModules: 7fac6030d37165c251a7bd4bde3333212544da3c - React-cxxreact: 0ead442ecaa248e7f71719e286510676495ae26d + React-Core: 92733c8280b1642afed7ebfb3c523feaec946ece + React-CoreModules: e2dfd87b6fdb9d969b16871655885a4d89a2a9f4 + React-cxxreact: d1a70e78543bb5b159fdaf6c52cadd33c1ae3244 React-debug: 78d7544d2750737ac3acc88cca2f457d081ec43d - React-defaultsnativemodule: 833b618f562a7798e7a814ce1ddc001464d7a3d0 - React-domnativemodule: c1ca50f25913f73d5e95d55ff5352e7f1d7ebcc8 - React-Fabric: 131631b99737169826d16290d5b90c53a150fc15 - React-FabricComponents: 1f6ce42418da316663f53b534bdebd23ec4be41f - React-FabricImage: b6ba029f882f1676cb1b59688fa39e1ef0814381 + React-defaultsnativemodule: b24e61fe2d5bb84501898683f9d13ff7fc02a9df + React-domnativemodule: 210ca3670f16ae92fbcff8da204750af8a7295af + React-Fabric: 4b3d03ea38646dcc80888253c2befca80526abed + React-FabricComponents: 38fcb6f5c08f8de9e693f2644d2da54ae4fbf6c8 + React-FabricImage: 1d37769002c13dfffa9f53557a173d56c9ade5e3 React-featureflags: 92dd7d0169ab0bf8ad404a5fe757c1ca7ccd74e8 - React-featureflagsnativemodule: 69bc086433eff3077b90f4ea17ab2083ad281868 - React-graphics: f09d013df7aef5551fdce4c99f2fe704c6c5b35a - React-hermes: 13e1c1c9222503bcd7ad450370c5a26dc9b46ebe - React-idlecallbacksnativemodule: f349708531f44d3db8ac79129d8e2b4d8cc3d1ff - React-ImageManager: e20f7c0291e5c9298b643c88b40db62c46a30ae4 - React-jserrorhandler: 79aa6ef93470ab9e8f4c6c6258dc662880b0bfb4 - React-jsi: 931610846e52e5d157f4bc3f71a14f9a53573abd - React-jsiexecutor: 3f5fb21d47c5c72c13a1710b288d78c8209a38f9 - React-jsinspector: d2653e42aae27f01f71f10ab87866cf092288e30 - React-jsitracing: fe93bab4193ec5528bcbdaf2f1b62475652490ad - React-logger: 9a0c4e1e41cd640ac49d69aacadab783f7e0096b - React-Mapbuffer: 6993c785c22a170c02489bc78ed207814cbd700f - React-microtasksnativemodule: 19230cd0933df6f6dc1336c9a9edc382d62638ae - react-native-audio-waveform: 37744a7dfd0ae7b7dbab14cfa59c1dbe2cb0b20c - react-native-safe-area-context: 9c33120e9eac7741a5364cc2d9f74665049b76b3 + React-featureflagsnativemodule: 8a6373d7b4ef3c08d82b60376f75bd189bfc8cb2 + React-graphics: 2b316fcf5b6c29ded7d53ae0007d1d129dc89510 + React-hermes: bf50c8272cb562300a54a621aa69dc12a0b4fcf2 + React-idlecallbacksnativemodule: 47df5b6649ca5e0046aa3e43e680452007b16871 + React-ImageManager: 83b8dc67e97cd5fe10cb715bd878aded16adb40f + React-jserrorhandler: ac08c5673dea69b08e11faf074fd602fbf9492cc + React-jsi: 19e77567e235d06b7e8f425d2a6c1e948ab286e9 + React-jsiexecutor: fe6ad8b9a2bf97e435fc1c969c80ed7f447ed68e + React-jsinspector: f321d958a5534b65b56f7806c674e159c28f7d69 + React-jsitracing: d358876acde46009f391228b932a5efe13c8895b + React-logger: 02e5802824aa9b15cb7df42e10a91abead83cd8d + React-Mapbuffer: 99bd566147aaa78e872568be53ebca8a4449ddae + React-microtasksnativemodule: 51e7813abf875408a0f367e473a65bbab6aa8481 + react-native-audio-waveform: 859f2101319f9616858e81604cd2e5d30876f4c5 + react-native-safe-area-context: 7e513d737b0b5c1d10bbe0e5fcc9f925a7be144c React-nativeconfig: cd0fbb40987a9658c24dab5812c14e5522a64929 - React-NativeModulesApple: 45187d13c68d47250a7416b18ff082c7cc07bff7 - React-perflogger: 15a7bcb6c46eae8a981f7add8c9f4172e2372324 - React-performancetimeline: 631ef8ac4246bca49c07b88cd1ad85ce460b97bf + React-NativeModulesApple: 4a9c304aa4fb086af32e8758ba892386d895b4d3 + React-perflogger: 721172bda31a65ce7b7a0c3bf3de96f12ef6f45d + React-performancetimeline: 46dbe9fd618ff882f59600dcd9fa923a9713cc3b React-RCTActionSheet: 25eb72eabade4095bfaf6cd9c5c965c76865daa8 - React-RCTAnimation: 04c987fa858fa16169f543d29edb4140bd35afa9 - React-RCTAppDelegate: b2707904e4f8ad92fd052e62684bf0c3b88381cc - React-RCTBlob: 1f214a7211632515805dd1f1b81fac70d12f812d - React-RCTFabric: 10f8b1ceac3c2feb3ddbede8a70c3410c68d79fe - React-RCTFBReactNativeSpec: 60d72b45a150ca35748b9a77028674b1e56a2e43 - React-RCTImage: e516d72739797fb7c1dac5c691f02a0f5445c290 - React-RCTLinking: 1e5554afe4f959696ad3285738c1510f2592f220 - React-RCTNetwork: 65e1e52c8614dcab342fa1eaec750ca818160e74 - React-RCTSettings: e86c204b481ef9264929fe00d1fdd04ce561748a - React-RCTText: 15f14d6f9b75e64ffe749c75e30ff047cf0fa1be - React-RCTVibration: 8d9078d5432972fe12d9f1526b38f504ad3d45cb + React-RCTAnimation: 8efbd0a4a71fd3dbe84e6d08b92bec5728b7524b + React-RCTAppDelegate: 8ff6da817adefd15d4e25ade53a477c344f9b213 + React-RCTBlob: 6056bd62a56a6d2dad55cdf195949db1de623e14 + React-RCTFabric: 949589de63c19b8b197555567fbc51eebd265bbc + React-RCTFBReactNativeSpec: 4214925b1c4829fb1e73bfbacb301244b522dc11 + React-RCTImage: 7b3f38c77e183bdcb43dbcd7b5842b96c814889a + React-RCTLinking: 6cca74db71b23f670b72e45603e615c2b72b2235 + React-RCTNetwork: 5791b0718eff20c12f6f3d62e2ad50cff4b5c8a0 + React-RCTSettings: 84154e31a232b5b03b6b7a89924a267c431ccf16 + React-RCTText: cd49cb4442ee7f64b0415b27745d2495cb40cfaa + React-RCTVibration: 2a7432e61d42f802716bd67edc793b5e5f58971a React-rendererconsistency: 7a81b08f01655b458d1de48ddd5b3f5988fd753f - React-rendererdebug: 28f591de2009cb053e21cbf87edb357e6b214147 + React-rendererdebug: a6547cf2f3f7bcdd8d36ff5e103145d83f5001d4 React-rncore: dd08c91cea25486f79012e32975c0ea26bd92760 - React-RuntimeApple: fc7a3fe49564bd6a5b8aef081341960212ab58d0 - React-RuntimeCore: 2f967e25ca18a85cff22d103fbe782828442eeb4 + React-RuntimeApple: ea09b4c38df2695e0cb3fa60a83db81d653a39fd + React-RuntimeCore: 3dc763d365a1f738d92cd942066dd347953733f3 React-runtimeexecutor: f9ae11481be048438640085c1e8266d6afebae44 - React-RuntimeHermes: e2160a175c7a34dad30b0e10d79e8d70da471beb - React-runtimescheduler: 07601cb38739f60ddb2f9efb854a13cfb48310dd + React-RuntimeHermes: 3bc16b5a5a756a292ad6f56968dfb8de643ae20b + React-runtimescheduler: 2e90401c400b62bb720d6ac028dcef803e30d888 React-timing: 0d0263a5d8ab6fc8c325efb54cee1d6a6f01d657 - React-utils: 015e250e7898047068792d4b532fed21f2eb1661 - ReactAppDependencyProvider: 3d947e9d62f351c06c71497e1be897e6006dc303 - ReactCodegen: 1baa534318b19e95fb0f02db0a1ae1e3c271944d - ReactCommon: 6014af4276bb2debc350e2620ef1bd856b4d981c - rn-fetch-blob: f065bb7ab7fb48dd002629f8bdcb0336602d3cba - RNFastImage: 5c9c9fed9c076e521b3f509fe79e790418a544e8 - RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 - RNGestureHandler: 9ae308f1850d9c296a1230db9d1b52858911916b + React-utils: 8905cd01f46755ea42268875d04c614a0d46431e + ReactAppDependencyProvider: 6e8d68583f39dc31ee65235110287277eb8556ef + ReactCodegen: c08a5113d9c9c895fe10f3c296f74c6b705a60a9 + ReactCommon: 1bd2dc684d7992acbf0dfee887b89a57a1ead86d + rn-fetch-blob: 25612b6d6f6e980c6f17ed98ba2f58f5696a51ca + RNFastImage: 462a183c4b0b6b26fdfd639e1ed6ba37536c3b87 + RNFS: 89de7d7f4c0f6bafa05343c578f61118c8282ed8 + RNGestureHandler: 8b1080a6db0be82dbca18550d6212b885bfab6b2 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 @@ -1946,4 +1946,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 6f704d99bbe3053bd858eef7cb4caed0084ae50b -COCOAPODS: 1.15.2 +COCOAPODS: 1.16.2 diff --git a/example/src/App.tsx b/example/src/App.tsx index 6243bee..a93d7e8 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -241,6 +241,14 @@ const LivePlayerComponent = ({ } }; + const handlePauseResumeRecording = async () => { + if (ref.current?.currentState === RecorderState.paused) { + await ref.current?.resumeRecord(); + } else { + await ref.current?.pauseRecord(); + } + }; + return ( { + console.log(`currentProgress ${currentProgress}`); + }} /> + {recorderState !== RecorderState.stopped && ( + + + {recorderState === RecorderState.paused ? 'Resume' : 'Pause'} + + + )} diff --git a/example/src/styles.ts b/example/src/styles.ts index 5e02853..ea8cd08 100644 --- a/example/src/styles.ts +++ b/example/src/styles.ts @@ -2,7 +2,6 @@ import { StyleSheet } from 'react-native'; import { Colors, scale } from './theme'; import { useColorScheme } from 'react-native'; - export type StyleSheetParams = | Partial<{ currentUser: boolean; @@ -20,7 +19,7 @@ const styles = (params: StyleSheetParams = {}) => StyleSheet.create({ appContainer: { flex: 1, - backgroundColor: useColorScheme() === "dark" ? Colors.gray : Colors.white, + backgroundColor: useColorScheme() === 'dark' ? Colors.gray : Colors.white, }, screenBackground: { flex: 1, @@ -79,6 +78,11 @@ const styles = (params: StyleSheetParams = {}) => width: scale(40), padding: scale(8), }, + recordPauseResumeAudioPressable: { + height: scale(40), + width: scale(40), + justifyContent: 'center', + }, liveWaveformContainer: { flexDirection: 'row', marginBottom: scale(8), diff --git a/ios/AudioRecorder.swift b/ios/AudioRecorder.swift index d571ca0..c46b327 100644 --- a/ios/AudioRecorder.swift +++ b/ios/AudioRecorder.swift @@ -17,6 +17,8 @@ public class AudioRecorder: NSObject, AVAudioRecorderDelegate{ var recordedDuration: CMTime = CMTime.zero private var timer: Timer? var updateFrequency = UpdateFrequency.medium + var emittingRecorderValueLastTime: DispatchTime? + var totalRecordingTime: Int = 0 private func createAudioRecordPath(fileNameFormat: String?) -> URL? { let format = DateFormatter() @@ -68,6 +70,7 @@ public class AudioRecorder: NSObject, AVAudioRecorderDelegate{ audioRecorder = try AVAudioRecorder(url: newPath, settings: settings as [String : Any]) audioRecorder?.delegate = self audioRecorder?.isMeteringEnabled = true + totalRecordingTime = 0 audioRecorder?.record() startListening() resolve(true) @@ -79,12 +82,16 @@ public class AudioRecorder: NSObject, AVAudioRecorderDelegate{ @objc func timerUpdate(_ sender:Timer) { if (audioRecorder?.isRecording ?? false) { - EventEmitter.sharedInstance.dispatch(name: Constants.onCurrentRecordingWaveformData, body: [Constants.currentDecibel: getDecibelLevel()]) + let currentTime = DispatchTime.now() + let timeFromLastEmitting = Int(currentTime.uptimeNanoseconds - emittingRecorderValueLastTime!.uptimeNanoseconds) / 1_000_000 + totalRecordingTime += timeFromLastEmitting + emittingRecorderValueLastTime = currentTime + EventEmitter.sharedInstance.dispatch(name: Constants.onCurrentRecordingWaveformData, body: [Constants.currentDecibel: getDecibelLevel(), Constants.progress: totalRecordingTime]) } } func startListening() { - stopListening() + emittingRecorderValueLastTime = DispatchTime.now() DispatchQueue.main.async { [weak self] in guard let strongSelf = self else {return } strongSelf.timer = Timer.scheduledTimer(timeInterval: TimeInterval((Float(strongSelf.updateFrequency.rawValue) / 1000)), target: strongSelf, selector: #selector(strongSelf.timerUpdate(_:)), userInfo: nil, repeats: true) @@ -123,11 +130,13 @@ public class AudioRecorder: NSObject, AVAudioRecorderDelegate{ public func pauseRecording(_ resolve: RCTPromiseResolveBlock) -> Void { audioRecorder?.pause() + stopListening() resolve(true) } public func resumeRecording(_ resolve: RCTPromiseResolveBlock) -> Void { audioRecorder?.record() + startListening() resolve(true) } diff --git a/src/components/Waveform/Waveform.tsx b/src/components/Waveform/Waveform.tsx index cef932b..a09f9f2 100644 --- a/src/components/Waveform/Waveform.tsx +++ b/src/components/Waveform/Waveform.tsx @@ -61,6 +61,7 @@ export const Waveform = forwardRef((props, ref) => { onPanStateChange = () => {}, onError = (_error: Error) => {}, onCurrentProgressChange = () => {}, + onRecordingProgressChange = () => {}, candleHeightScale = 3, onChangeWaveformLoadState = (_state: boolean) => {}, showsHorizontalScrollIndicator = false, @@ -504,6 +505,9 @@ export const Waveform = forwardRef((props, ref) => { const traceRecorderWaveformValue = onCurrentRecordingWaveformData( result => { if (mode === 'live') { + if (!isNil(onRecordingProgressChange)) { + (onRecordingProgressChange as Function)(result.progress); + } if (!isNil(result.currentDecibel)) { setWaveform((previousWaveform: number[]) => { // Add the new decibel to the waveform diff --git a/src/components/Waveform/WaveformTypes.ts b/src/components/Waveform/WaveformTypes.ts index c7eaa81..7b1b85f 100644 --- a/src/components/Waveform/WaveformTypes.ts +++ b/src/components/Waveform/WaveformTypes.ts @@ -36,6 +36,7 @@ export interface LiveWaveform extends BaseWaveform { showsHorizontalScrollIndicator?: boolean; maxCandlesToRender?: number; onRecorderStateChange?: (recorderState: RecorderState) => void; + onRecordingProgressChange?: (currentProgress: number) => void; } export type IWaveform = StaticWaveform | LiveWaveform; diff --git a/src/types/AudioWaveformTypes.ts b/src/types/AudioWaveformTypes.ts index 84ed169..2fd1bcc 100644 --- a/src/types/AudioWaveformTypes.ts +++ b/src/types/AudioWaveformTypes.ts @@ -69,6 +69,7 @@ export interface IOnCurrentExtractedWaveForm extends IPlayerKey { export interface IOnCurrentRecordingWaveForm { currentDecibel: number; + progress: number; } export interface ISetPlaybackSpeed extends IPlayerKey {