From cbba6533a75d2854d2741e75d5469cbfbc9789fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cleehom6666=E2=80=9D?= Date: Thu, 21 Nov 2024 16:09:40 +0800 Subject: [PATCH] feat: add HarmonyOS support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: “leehom6666” --- .gitignore | 36 -- NativeAudio.ts | 43 +++ OAT.xml | 55 +++ README.md | 161 +------- harmony/audio/.gitignore | 5 + harmony/audio/build-profile.json5 | 8 + harmony/audio/hvigorfile.ts | 2 + harmony/audio/index.ets | 6 + harmony/audio/oh-package.json5 | 12 + harmony/audio/src/main/.gitignore | 1 + harmony/audio/src/main/cpp/AudioPackage.h | 16 + harmony/audio/src/main/cpp/CMakeLists.txt | 9 + .../generated/BaseReactNativeAudioPackage.h | 65 ++++ .../RNOH/generated/turbo_modules/RTNAudio.cpp | 23 ++ .../RNOH/generated/turbo_modules/RTNAudio.h | 16 + .../react_native_audio/ComponentDescriptors.h | 22 ++ .../react_native_audio/EventEmitters.cpp | 18 + .../react_native_audio/EventEmitters.h | 19 + .../components/react_native_audio/Props.cpp | 21 ++ .../components/react_native_audio/Props.h | 20 + .../react_native_audio/ShadowNodes.cpp | 19 + .../react_native_audio/ShadowNodes.h | 25 ++ .../components/react_native_audio/States.cpp | 18 + .../components/react_native_audio/States.h | 23 ++ harmony/audio/src/main/ets/AudioModule.ts | 48 +++ harmony/audio/src/main/ets/AudioPackage.ts | 29 ++ .../audio/src/main/ets/AudioRecordManager.ts | 352 ++++++++++++++++++ harmony/audio/src/main/ets/AudioType.ts | 26 ++ harmony/audio/src/main/ets/StopWatch.ts | 44 +++ .../src/main/ets/generated/components/ts.ts | 5 + .../audio/src/main/ets/generated/index.ets | 5 + harmony/audio/src/main/ets/generated/ts.ts | 6 + .../ets/generated/turboModules/RTNAudio.ts | 32 ++ .../src/main/ets/generated/turboModules/ts.ts | 5 + harmony/audio/src/main/module.json5 | 19 + .../main/resources/base/element/string.json | 20 + .../main/resources/en_US/element/string.json | 8 + .../main/resources/zh_CN/element/string.json | 8 + harmony/audio/ts.ts | 7 + index.js | 120 +++--- package.json | 28 +- 41 files changed, 1164 insertions(+), 241 deletions(-) delete mode 100644 .gitignore create mode 100644 NativeAudio.ts create mode 100644 OAT.xml create mode 100644 harmony/audio/.gitignore create mode 100644 harmony/audio/build-profile.json5 create mode 100644 harmony/audio/hvigorfile.ts create mode 100644 harmony/audio/index.ets create mode 100644 harmony/audio/oh-package.json5 create mode 100644 harmony/audio/src/main/.gitignore create mode 100644 harmony/audio/src/main/cpp/AudioPackage.h create mode 100644 harmony/audio/src/main/cpp/CMakeLists.txt create mode 100644 harmony/audio/src/main/cpp/generated/RNOH/generated/BaseReactNativeAudioPackage.h create mode 100644 harmony/audio/src/main/cpp/generated/RNOH/generated/turbo_modules/RTNAudio.cpp create mode 100644 harmony/audio/src/main/cpp/generated/RNOH/generated/turbo_modules/RTNAudio.h create mode 100644 harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ComponentDescriptors.h create mode 100644 harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/EventEmitters.cpp create mode 100644 harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/EventEmitters.h create mode 100644 harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/Props.cpp create mode 100644 harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/Props.h create mode 100644 harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ShadowNodes.cpp create mode 100644 harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ShadowNodes.h create mode 100644 harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/States.cpp create mode 100644 harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/States.h create mode 100644 harmony/audio/src/main/ets/AudioModule.ts create mode 100644 harmony/audio/src/main/ets/AudioPackage.ts create mode 100644 harmony/audio/src/main/ets/AudioRecordManager.ts create mode 100644 harmony/audio/src/main/ets/AudioType.ts create mode 100644 harmony/audio/src/main/ets/StopWatch.ts create mode 100644 harmony/audio/src/main/ets/generated/components/ts.ts create mode 100644 harmony/audio/src/main/ets/generated/index.ets create mode 100644 harmony/audio/src/main/ets/generated/ts.ts create mode 100644 harmony/audio/src/main/ets/generated/turboModules/RTNAudio.ts create mode 100644 harmony/audio/src/main/ets/generated/turboModules/ts.ts create mode 100644 harmony/audio/src/main/module.json5 create mode 100644 harmony/audio/src/main/resources/base/element/string.json create mode 100644 harmony/audio/src/main/resources/en_US/element/string.json create mode 100644 harmony/audio/src/main/resources/zh_CN/element/string.json create mode 100644 harmony/audio/ts.ts diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 9c60292..0000000 --- a/.gitignore +++ /dev/null @@ -1,36 +0,0 @@ -# OSX -# -.DS_Store - -# Xcode -# -build/ -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata -*.xccheckout -*.moved-aside -DerivedData -*.hmap -*.ipa -*.xcuserstate -project.xcworkspace - -# Cocoapods -Pods/ - -# node.js -# -node_modules/ -npm-debug.log - -android/gradle/ -android/gradlew -android/gradlew.bat -.idea diff --git a/NativeAudio.ts b/NativeAudio.ts new file mode 100644 index 0000000..1b6d77f --- /dev/null +++ b/NativeAudio.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +import type { TurboModule } from "react-native/Libraries/TurboModule/RCTExport"; +import { TurboModuleRegistry } from "react-native"; +import type { Int32, Double } from 'react-native/Libraries/Types/CodegenTypes'; + +type AudioQualityType = 'Low' | 'Medium' | 'High'; + +export interface RecordingOptions { + SampleRate: Double, + Channels: Int32, + AudioQuality?: AudioQualityType, + AudioEncoding: string, + MeteringEnabled?: boolean, + MeasurementMode?: boolean, + AudioEncodingBitRate: Double, + IncludeBase64: boolean, + OutputFormat: string, + AudioSource: Int32 +} + +export interface PathMap { + FilesDirectoryPath: string, + CacheDirectoryPath: string, + TempsDirectoryPath: string, +} + +export interface Spec extends TurboModule { + prepareRecordingAtPath: (path: string, options: RecordingOptions) => Promise; + requestAuthorization: () => Promise; + startRecording: () => Promise; + pauseRecording: () => Promise; + resumeRecording: () => Promise; + stopRecording: () => Promise; + getAllPath: () => PathMap; + checkAuthorizationStatus: () => Promise; +} + +export default TurboModuleRegistry.getEnforcing("RTNAudio"); \ No newline at end of file diff --git a/OAT.xml b/OAT.xml new file mode 100644 index 0000000..8207885 --- /dev/null +++ b/OAT.xml @@ -0,0 +1,55 @@ + + + + LICENSE + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index f4df63d..cb83064 100644 --- a/README.md +++ b/README.md @@ -1,162 +1,15 @@ +# @react-native-ohos/react-native-audio -Record audio in iOS or Android React Native apps. +This project is based on [react-native-audio](https://github.com/jsierles/react-native-audio) -## MAINTENANCE STATUS +## Documentation -This project is no longer actively maintained by me, the original author. I will not be answering issues or merging any more PRs. If someone is interested in taking over the library permanently, let me know! +[中文](https://gitee.com/react-native-oh-library/usage-docs/blob/master/zh-cn/react-native-audio.md) -## BREAKING CHANGES +[English](https://gitee.com/react-native-oh-library/usage-docs/blob/master/en/react-native-audio.md) -For React Native >= 0.47.2, use v3.4.0 and up. -For React Native >= 0.40, use v3.1.0 up til 3.2.2. -For React Native <= 0.39, use v3.0.0 or lower. -v4.0 introduced a breaking change to the API to introduce distinct pause and resume methods. +## License -v3.x removed playback support in favor of using more mature libraries like [react-native-sound](https://github.com/zmxv/react-native-sound). If you need to play -from the network, please submit a PR to that project or try `react-native-video`. +This library is licensed under [The MIT License (MIT)](https://gitee.com/openharmony-sig/rntpc_react-native-audio/blob/master/LICENSE). -### Installation - -Install the npm package and link it to your project: - -``` -npm install react-native-audio --save -react-native link react-native-audio -``` - -On *iOS* you need to add a usage description to `Info.plist`: - -``` -NSMicrophoneUsageDescription -This sample uses the microphone to record your speech and convert it to text. -``` - -On *Android* you need to add a permission to `AndroidManifest.xml`: - -``` - -``` - -### Manual Installation - -This is not necessary if you have used `react-native link` - -#### Android - -Edit `android/settings.gradle` to declare the project directory: -``` -include ':react-native-audio' -project(':react-native-audio').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-audio/android') -``` - -Edit `android/app/build.gradle` to declare the project dependency: -``` -dependencies { - ... - compile project(':react-native-audio') -} -``` - -Edit `android/app/src/main/java/.../MainApplication.java` to register the native module: - -```java -... -import com.rnim.rn.audio.ReactNativeAudioPackage; // <-- New -... - -public class MainApplication extends Application implements ReactApplication { - ... - @Override - protected List getPackages() { - return Arrays.asList( - new MainReactPackage(), - new ReactNativeAudioPackage() // <-- New - ); - } -``` - -#### iOS - -Drag `node_modules/react-native-audio/ios/RNAudio.xcoderproj` into your project's Libraries on Xcode. - -Add `libRNAudio.a` into Link Binary With Libraries from Xcode - Build Phases. - - -### Running the Sample App - -In the `AudioExample` directory: - -``` -npm install -react-native run-ios -react-native run-android -``` - -### Usage - -To record in AAC format, at 22050 KHz in low quality mono: - -``` -import {AudioRecorder, AudioUtils} from 'react-native-audio'; -let audioPath = AudioUtils.DocumentDirectoryPath + '/test.aac'; - -AudioRecorder.prepareRecordingAtPath(audioPath, { - SampleRate: 22050, - Channels: 1, - AudioQuality: "Low", - AudioEncoding: "aac" -}); -``` - -`AudioQuality` is supported on iOS. `Low`, `Medium`, and `High` will translate to `AVAudioQualityLow`, `AVAudioQualityMedium`, and `AVAudioQualityHigh` respectively. - -#### Cross-platform options - -``` -SampleRate: int -Channels: int -AudioQuality: string -AudioEncoding: string -IncludeBase64: boolean -``` - -Encodings supported on iOS: `lpcm, ima4, aac, MAC3, MAC6, ulaw, alaw, mp1, mp2, alac, amr` -Encodings supported on Android: `aac, aac_eld, amr_nb, amr_wb, he_aac, vorbis` - -Use the `IncludeBase64` boolean to include the `base64` encoded recording on the `AudioRecorder.onFinished` event object. Please use it with care: passing large amounts of data over the bridge, from native to Javascript, can use lots of memory and cause slow performance. - -If you want to upload the audio, it might be best to do it on the native thread with a package like [React Native Fetch Blob](https://github.com/joltup/react-native-fetch-blob). - -#### iOS-only fields - -Use `MeteringEnabled` boolean to enable audio metering. The following values are available on the recording progress object. - -| Name | Related AVAudioRecorder parameter | Description | -|------|-----------------------------------|-------------| -|currentMetering| averagePowerForChannel | The current average power, in decibels, for the sound being recorded. A return value of 0 dB indicates full scale, or maximum power; a return value of -160 dB indicates minimum power (that is, near silence). If the signal provided to the audio recorder exceeds ±full scale, then the return value may exceed 0 (that is, it may enter the positive range).| -|currentPeakMetering | peakPowerForChannel | The current peak power, in decibels, for the sound being recorded. A return value of 0 dB indicates full scale, or maximum power; a return value of -160 dB indicates minimum power (that is, near silence). If the signal provided to the audio recorder exceeds ±full scale, then the return value may exceed 0 (that is, it may enter the positive range).| - -For example: - -```js -AudioRecorder.onProgress = (data) => { - console.log(data.currentMetering, data.currentPeakMetering) -}; -``` - -#### Android-only fields - -AudioEncodingBitRate: int - -OutputFormat: string, `mpeg_4, aac_adts, amr_nb, amr_wb, three_gpp, webm` - -AudioSource: int (constants) (Possible values: AudioSource.DEFAULT, AudioSource.MIC, AudioSource.VOICE_UPLINK, AudioSource.VOICE_DOWNLINK, AudioSource.VOICE_CALL, AudioSource.CAMCORDER, AudioSource.VOICE_RECOGNITION, AudioSource.VOICE_COMMUNICATION, AudioSource.REMOTE_SUBMIX, AudioSource.UNPROCESSED) - -See [the example](https://github.com/jsierles/react-native-audio/blob/master/AudioExample/index.ios.js) for more details. For playing audio check out [React Native Sound](https://github.com/zmxv/react-native-sound) - -MP3 recording is *not supported* since the underlying platforms do not support it. - -Thanks to Brent Vatne, Johannes Lumpe, Kureev Alexey, Matthew Hartman and Rakan Nimer for their assistance. - -Progress tracking code borrowed from https://github.com/brentvatne/react-native-video. diff --git a/harmony/audio/.gitignore b/harmony/audio/.gitignore new file mode 100644 index 0000000..346aec5 --- /dev/null +++ b/harmony/audio/.gitignore @@ -0,0 +1,5 @@ +/node_modules +/.preview +/build +/.cxx +/oh_modules \ No newline at end of file diff --git a/harmony/audio/build-profile.json5 b/harmony/audio/build-profile.json5 new file mode 100644 index 0000000..b823c6e --- /dev/null +++ b/harmony/audio/build-profile.json5 @@ -0,0 +1,8 @@ +{ + "apiType": "stageMode", + "targets": [ + { + "name": "default" + } + ] +} diff --git a/harmony/audio/hvigorfile.ts b/harmony/audio/hvigorfile.ts new file mode 100644 index 0000000..47e6e1f --- /dev/null +++ b/harmony/audio/hvigorfile.ts @@ -0,0 +1,2 @@ +// Script for compiling build behavior. It is built in the build plug-in and cannot be modified currently. +export { harTasks } from '@ohos/hvigor-ohos-plugin'; \ No newline at end of file diff --git a/harmony/audio/index.ets b/harmony/audio/index.ets new file mode 100644 index 0000000..368e548 --- /dev/null +++ b/harmony/audio/index.ets @@ -0,0 +1,6 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +export * from './ts'; \ No newline at end of file diff --git a/harmony/audio/oh-package.json5 b/harmony/audio/oh-package.json5 new file mode 100644 index 0000000..b3eecd0 --- /dev/null +++ b/harmony/audio/oh-package.json5 @@ -0,0 +1,12 @@ +{ + license: 'MIT', + types: '', + name: '@react-native-ohos/react-native-audio', + description: '', + main: 'index.ets', + type: 'module', + version: '4.2.3-rc1', + dependencies: { + "@rnoh/react-native-openharmony": "^0.72.38" + }, +} \ No newline at end of file diff --git a/harmony/audio/src/main/.gitignore b/harmony/audio/src/main/.gitignore new file mode 100644 index 0000000..1d74e21 --- /dev/null +++ b/harmony/audio/src/main/.gitignore @@ -0,0 +1 @@ +.vscode/ diff --git a/harmony/audio/src/main/cpp/AudioPackage.h b/harmony/audio/src/main/cpp/AudioPackage.h new file mode 100644 index 0000000..b383f50 --- /dev/null +++ b/harmony/audio/src/main/cpp/AudioPackage.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +#pragma once + +#include "generated/RNOH/generated/BaseReactNativeAudioPackage.h" +namespace rnoh { + +class AudioPackage : public BaseReactNativeAudioPackage { + using Super = BaseReactNativeAudioPackage; + using Super::Super; +}; +} // namespace rnoh \ No newline at end of file diff --git a/harmony/audio/src/main/cpp/CMakeLists.txt b/harmony/audio/src/main/cpp/CMakeLists.txt new file mode 100644 index 0000000..84aecfd --- /dev/null +++ b/harmony/audio/src/main/cpp/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.13) +set(CMAKE_VERBOSE_MAKEFILE on) + +set(rnoh_audio_generated_dir "${CMAKE_CURRENT_SOURCE_DIR}/generated") +file(GLOB_RECURSE rnoh_audio_generated_SRC "${rnoh_audio_generated_dir}/**/*.cpp") +file(GLOB rnoh_audio_SRC CONFIGURE_DEPENDS *.cpp) +add_library(rnoh_audio SHARED ${rnoh_audio_SRC} ${rnoh_audio_generated_SRC}) +target_include_directories(rnoh_audio PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${rnoh_audio_generated_dir}) +target_link_libraries(rnoh_audio PUBLIC rnoh) diff --git a/harmony/audio/src/main/cpp/generated/RNOH/generated/BaseReactNativeAudioPackage.h b/harmony/audio/src/main/cpp/generated/RNOH/generated/BaseReactNativeAudioPackage.h new file mode 100644 index 0000000..a631a2d --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/RNOH/generated/BaseReactNativeAudioPackage.h @@ -0,0 +1,65 @@ +/** + * This code was generated by "react-native codegen-lib-harmony" + */ + +#pragma once + +#include "RNOH/Package.h" +#include "RNOH/ArkTSTurboModule.h" +#include "RNOH/generated/turbo_modules/RTNAudio.h" + +namespace rnoh { + +class BaseReactNativeAudioPackageTurboModuleFactoryDelegate : public TurboModuleFactoryDelegate { + public: + SharedTurboModule createTurboModule(Context ctx, const std::string &name) const override { + if (name == "RTNAudio") { + return std::make_shared(ctx, name); + } + return nullptr; + }; +}; + +class BaseReactNativeAudioPackageEventEmitRequestHandler : public EventEmitRequestHandler { + public: + void handleEvent(Context const &ctx) override { + auto eventEmitter = ctx.shadowViewRegistry->getEventEmitter(ctx.tag); + if (eventEmitter == nullptr) { + return; + } + + std::vector supportedEventNames = { + }; + if (std::find(supportedEventNames.begin(), supportedEventNames.end(), ctx.eventName) != supportedEventNames.end()) { + eventEmitter->dispatchEvent(ctx.eventName, ArkJS(ctx.env).getDynamic(ctx.payload)); + } + } +}; + + +class BaseReactNativeAudioPackage : public Package { + public: + BaseReactNativeAudioPackage(Package::Context ctx) : Package(ctx){}; + + std::unique_ptr createTurboModuleFactoryDelegate() override { + return std::make_unique(); + } + + std::vector createComponentDescriptorProviders() override { + return { + }; + } + + ComponentJSIBinderByString createComponentJSIBinderByName() override { + return { + }; + }; + + EventEmitRequestHandlers createEventEmitRequestHandlers() override { + return { + std::make_shared(), + }; + } +}; + +} // namespace rnoh diff --git a/harmony/audio/src/main/cpp/generated/RNOH/generated/turbo_modules/RTNAudio.cpp b/harmony/audio/src/main/cpp/generated/RNOH/generated/turbo_modules/RTNAudio.cpp new file mode 100644 index 0000000..2d65d62 --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/RNOH/generated/turbo_modules/RTNAudio.cpp @@ -0,0 +1,23 @@ +/** + * This code was generated by "react-native codegen-lib-harmony" + */ + +#include "RTNAudio.h" + +namespace rnoh { +using namespace facebook; + +RTNAudio::RTNAudio(const ArkTSTurboModule::Context ctx, const std::string name) : ArkTSTurboModule(ctx, name) { + methodMap_ = { + ARK_ASYNC_METHOD_METADATA(prepareRecordingAtPath, 2), + ARK_ASYNC_METHOD_METADATA(requestAuthorization, 0), + ARK_ASYNC_METHOD_METADATA(startRecording, 0), + ARK_ASYNC_METHOD_METADATA(pauseRecording, 0), + ARK_ASYNC_METHOD_METADATA(resumeRecording, 0), + ARK_ASYNC_METHOD_METADATA(stopRecording, 0), + ARK_METHOD_METADATA(getAllPath, 0), + ARK_ASYNC_METHOD_METADATA(checkAuthorizationStatus, 0), + }; +} + +} // namespace rnoh diff --git a/harmony/audio/src/main/cpp/generated/RNOH/generated/turbo_modules/RTNAudio.h b/harmony/audio/src/main/cpp/generated/RNOH/generated/turbo_modules/RTNAudio.h new file mode 100644 index 0000000..23f2cc0 --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/RNOH/generated/turbo_modules/RTNAudio.h @@ -0,0 +1,16 @@ +/** + * This code was generated by "react-native codegen-lib-harmony" + */ + +#pragma once + +#include "RNOH/ArkTSTurboModule.h" + +namespace rnoh { + +class JSI_EXPORT RTNAudio : public ArkTSTurboModule { + public: + RTNAudio(const ArkTSTurboModule::Context ctx, const std::string name); +}; + +} // namespace rnoh diff --git a/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ComponentDescriptors.h b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ComponentDescriptors.h new file mode 100644 index 0000000..52c479e --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ComponentDescriptors.h @@ -0,0 +1,22 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateComponentDescriptorH.js + */ + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + + + +} // namespace react +} // namespace facebook diff --git a/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/EventEmitters.cpp b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/EventEmitters.cpp new file mode 100644 index 0000000..4808204 --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/EventEmitters.cpp @@ -0,0 +1,18 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateEventEmitterCpp.js + */ + +#include + + +namespace facebook { +namespace react { + +} // namespace react +} // namespace facebook diff --git a/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/EventEmitters.h b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/EventEmitters.h new file mode 100644 index 0000000..5a51f7b --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/EventEmitters.h @@ -0,0 +1,19 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateEventEmitterH.js + */ +#pragma once + +#include + + +namespace facebook { +namespace react { + +} // namespace react +} // namespace facebook diff --git a/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/Props.cpp b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/Props.cpp new file mode 100644 index 0000000..9364e95 --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/Props.cpp @@ -0,0 +1,21 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GeneratePropsCpp.js + */ + +#include +#include +#include + +namespace facebook { +namespace react { + + + +} // namespace react +} // namespace facebook diff --git a/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/Props.h b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/Props.h new file mode 100644 index 0000000..cdd4932 --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/Props.h @@ -0,0 +1,20 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GeneratePropsH.js + */ +#pragma once + + + +namespace facebook { +namespace react { + + + +} // namespace react +} // namespace facebook diff --git a/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ShadowNodes.cpp b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ShadowNodes.cpp new file mode 100644 index 0000000..b6b502c --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ShadowNodes.cpp @@ -0,0 +1,19 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateShadowNodeCpp.js + */ + +#include + +namespace facebook { +namespace react { + + + +} // namespace react +} // namespace facebook diff --git a/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ShadowNodes.h b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ShadowNodes.h new file mode 100644 index 0000000..faa8cdc --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/ShadowNodes.h @@ -0,0 +1,25 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateShadowNodeH.js + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace facebook { +namespace react { + + + +} // namespace react +} // namespace facebook diff --git a/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/States.cpp b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/States.cpp new file mode 100644 index 0000000..96fbbc0 --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/States.cpp @@ -0,0 +1,18 @@ + +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateStateCpp.js + */ +#include + +namespace facebook { +namespace react { + + + +} // namespace react +} // namespace facebook diff --git a/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/States.h b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/States.h new file mode 100644 index 0000000..873910f --- /dev/null +++ b/harmony/audio/src/main/cpp/generated/react/renderer/components/react_native_audio/States.h @@ -0,0 +1,23 @@ +/** + * This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen). + * + * Do not edit this file as changes may cause incorrect behavior and will be lost + * once the code is regenerated. + * + * @generated by codegen project: GenerateStateH.js + */ +#pragma once + +#ifdef ANDROID +#include +#include +#include +#endif + +namespace facebook { +namespace react { + + + +} // namespace react +} // namespace facebook \ No newline at end of file diff --git a/harmony/audio/src/main/ets/AudioModule.ts b/harmony/audio/src/main/ets/AudioModule.ts new file mode 100644 index 0000000..b6dd7af --- /dev/null +++ b/harmony/audio/src/main/ets/AudioModule.ts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +import { TurboModule } from "@rnoh/react-native-openharmony/ts"; +import type { TurboModuleContext } from "@rnoh/react-native-openharmony/ts"; +import { AudioRecordManager } from './AudioRecordManager'; +import { RecordingOptions, PathMap } from './AudioType'; +import { TM } from "./generated/ts"; + +export class AudioModule extends TurboModule implements TM.RTNAudio.Spec { + ctx!: TurboModuleContext; + audioRecorderManager: AudioRecordManager = new AudioRecordManager(this.ctx); + + prepareRecordingAtPath(path: string, options: RecordingOptions): Promise { + return this.audioRecorderManager.prepareRecordingAtPath(path, options); + } + + startRecording(): Promise { + return this.audioRecorderManager.startRecording(); + } + + pauseRecording(): Promise { + return this.audioRecorderManager.pauseRecording(); + } + + resumeRecording(): Promise { + return this.audioRecorderManager.resumeRecording(); + } + + stopRecording(): Promise { + return this.audioRecorderManager.stopRecording(); + } + + requestAuthorization(): Promise { + return this.audioRecorderManager.requestAuthorization(); + } + + getAllPath(): PathMap { + return this.audioRecorderManager.getAllPath(); + } + + checkAuthorizationStatus(): Promise { + return this.audioRecorderManager.checkAuthorizationStatus(); + } +} diff --git a/harmony/audio/src/main/ets/AudioPackage.ts b/harmony/audio/src/main/ets/AudioPackage.ts new file mode 100644 index 0000000..7cdc552 --- /dev/null +++ b/harmony/audio/src/main/ets/AudioPackage.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +import { RNPackage, TurboModulesFactory } from '@rnoh/react-native-openharmony/ts'; +import type { TurboModule, TurboModuleContext } from '@rnoh/react-native-openharmony/ts'; +import { AudioModule } from './AudioModule'; +import { TM } from "./generated/ts"; + +class AudioModulesFactory extends TurboModulesFactory { + createTurboModule(name: string): TurboModule | null { + if (name === TM.RTNAudio.NAME) { + return new AudioModule(this.ctx) + } + return null; + } + + hasTurboModule(name: string): boolean { + return name === TM.RTNAudio.NAME; + } +} + +export class AudioPackage extends RNPackage { + createTurboModulesFactory(ctx: TurboModuleContext): TurboModulesFactory { + return new AudioModulesFactory(ctx); + } +} diff --git a/harmony/audio/src/main/ets/AudioRecordManager.ts b/harmony/audio/src/main/ets/AudioRecordManager.ts new file mode 100644 index 0000000..532829a --- /dev/null +++ b/harmony/audio/src/main/ets/AudioRecordManager.ts @@ -0,0 +1,352 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +import media from '@ohos.multimedia.media'; +import { BusinessError } from '@ohos.base'; +import promptAction from '@ohos.promptAction'; +import { RecordingOptions, PathMap } from './AudioType'; +import fs from '@ohos.file.fs'; +import common from '@ohos.app.ability.common'; +import bundleManager from '@ohos.bundle.bundleManager'; +import abilityAccessCtrl, { Permissions } from '@ohos.abilityAccessCtrl'; +import type { RNOHContext,RNOHLogger } from '@rnoh/react-native-openharmony/ts'; +import { stopWatch } from './StopWatch'; +import util from '@ohos.util'; + +const TIP_BOTTOM = 140; +const TOAST_DURATION = 1500; +const PERMISSION_LIST: Array = ['ohos.permission.MICROPHONE']; + +enum AVRecorderStateEnum { + IDLE = 'idle', + PREPARED = 'prepared', + STARTED = 'started', + PAUSED = 'paused', + STOPPED = 'stopped', + RELEASED = 'released', + ERROR = 'error' +} + +export class AudioRecordManager { + private context: common.UIAbilityContext + private ctx!: RNOHContext; + private avRecorder: media.AVRecorder = {} as media.AVRecorder; + private isRecording: boolean = false; + private avProfile: media.AVRecorderProfile = { + audioBitrate: 100000, //音频比特率 + audioChannels: 2, //音频声道数 + audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac + audioSampleRate: 48000, // 音频采样率 + fileFormat: media.ContainerFormatType.CFT_MPEG_4A, //封装格式,当前只支持m4a + }; + private avConfig: media.AVRecorderConfig = { + audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, //音频输入源,这里设置为麦克风(1) + profile: this.avProfile, + url: 'fd://35', //使用fs.openSync()获取文件fd + }; + private state: media.AVRecorderState = AVRecorderStateEnum.IDLE; + private timer: number | null = null; + private file: fs.File; + private includeBase64: boolean = false; + private filePath: string = ''; + private logger: RNOHLogger + + constructor(ctx: RNOHContext) { + this.ctx = ctx; + this.context = ctx.uiAbilityContext + this.logger = ctx.logger.clone("AudioRecorder"); + } + + //开始录制对应的配置 + async prepareRecordingAtPath(path: string, options: RecordingOptions): Promise { + if (this.isRecording) { + this.logger.error('Please call stopRecording before starting recording.'); + return; + } + if (path === '') { + this.logger.error('Invalid path.'); + return; + } + const isAuthorization = await this.checkAuthorizationStatus(); + if (!isAuthorization) { + this.logger.error('Please obtain microphone authorization first.'); + return; + } + try { + //创建录制实例 + this.avRecorder = await media.createAVRecorder(); + //监听状态改变 + this.avRecorder.on('stateChange', (state: media.AVRecorderState) => { + this.state = state; + this.logger.info(`current state is ${state}.`); + }) + //错误上报信息 + this.avRecorder.on('error', (error: BusinessError) => { + this.logger.error(`AudioRecorder failed, code is ${error?.code}, message is ${error?.message}.`); + if (error.code === 5400107) { + this.logger.error(`Please call stopRecording before starting recording.`); + } + }) + //初始化音频参数 + this.avProfile.audioSampleRate = options.SampleRate; + this.avProfile.audioChannels = options.Channels; + this.avProfile.audioCodec = this.getAudioCodecFormatString(options.AudioEncoding); + this.avProfile.audioBitrate = options.AudioEncodingBitRate; + this.avProfile.fileFormat = this.getFileFormatFormatString(options.OutputFormat); + this.avConfig.audioSourceType = this.getAudioSourceFormatString(options.AudioSource); + //获取应用文件路径 + this.filePath = path; + this.file = fs.openSync(path, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); + this.avConfig.url = `fd://${this.file.fd}`; + this.includeBase64 = options.IncludeBase64; + await this.avRecorder.prepare(this.avConfig).then((res) => { + this.logger.info(`${res}.`); + }).catch((err) => { + this.logger.error(`${err}.`); + }); + this.logger.info(`Recording is prepared.`); + this.logger.debug('Recording is prepared.'); + } catch (error) { + this.logger.error(`${JSON.stringify(error)}.`); + } + } + + //请求麦克风权限 + async requestAuthorization(): Promise { + let grantStatus: boolean = await this.checkAuthorizationStatus(PERMISSION_LIST[0]); + let authorizationStatus: boolean = grantStatus; + if (grantStatus) { + //已经授权,可以继续访问目标操作 + this.logger.info(`Already authorized.`); + } else { + this.logger.info(`No authorization.`); + await this.requestAuth(PERMISSION_LIST); + authorizationStatus = await this.checkAuthorizationStatus(); + } + return authorizationStatus; + } + + //请求权限 + async requestAuth(permissions: Array) { + let atManager = abilityAccessCtrl.createAtManager(); + try { + const data = await atManager.requestPermissionsFromUser(this.context, permissions); + let grantStatus: Array = data.authResults; + let length: number = grantStatus.length; + for (let i = 0; i < length; i++) { + if (grantStatus[i] === 0) { + //用户授权,可以继续访问目标操作 + this.logger.info(`Authorization successful.`); + this.logger.debug('Authorization successful.'); + } else { + //用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限 + this.logger.info(`Deny authorization.`); + this.logger.debug('Deny authorization! Recording requires authorization. Enter the system settings and turn on microphone permissions.'); + return; + } + } + } catch (error) { + this.logger.error(`requestPermissionsFromUser failed, code is ${error?.code}, message is ${error?.message}.`); + } + } + + //检查是否授权 + async checkAuthorizationStatus(permission: Permissions = PERMISSION_LIST[0]): Promise { + let atManager = abilityAccessCtrl.createAtManager(); + let grantStatus: abilityAccessCtrl.GrantStatus; + //获取应用程序的accessTokenID + let tokenId: number; + try { + let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); + let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo; + tokenId = appInfo.accessTokenId; + } catch (error) { + this.logger.error(`getBundleInfoForSelf failed, code is ${error?.code}, message is ${error?.message}.`); + } + //检查应用是否被授予权限 + try { + grantStatus = await atManager.checkAccessToken(tokenId, permission); + } catch (error) { + this.logger.error(`checkAccessToken failed, code is ${error?.code}, message is ${error?.message}.`); + } + return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED; + } + + //格式化音频输入源 + getAudioSourceFormatString(audioSource: number) { + switch (audioSource) { + case 0: + return media.AudioSourceType.AUDIO_SOURCE_TYPE_DEFAULT; + case 1: + return media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC; + default: + this.logger.debug(`Using media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC : ${media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC}.`); + return media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC; + } + } + + //格式化音频编码格式,当前仅支持aac + getAudioCodecFormatString(audioEncoding: string) { + switch (audioEncoding) { + case 'aac': + return media.CodecMimeType.AUDIO_AAC; + default: + this.logger.debug(`Using media.CodecMimeType.AUDIO_AAC : ${media.CodecMimeType.AUDIO_AAC}.`); + return media.CodecMimeType.AUDIO_AAC; + } + } + + //格式化封装格式,当前仅支持m4a + getFileFormatFormatString(fileFormat: string) { + switch (fileFormat) { + case 'm4a': + return media.ContainerFormatType.CFT_MPEG_4A; + default: + this.logger.debug(`Using media.ContainerFormatType.CFT_MPEG_4A : ${media.ContainerFormatType.CFT_MPEG_4A}.`); + return media.ContainerFormatType.CFT_MPEG_4A; + } + } + + //开始录制 + public async startRecording() { + try { + if (this.avRecorder.state === AVRecorderStateEnum.STARTED || this.avRecorder.state === AVRecorderStateEnum.PAUSED) { + this.logger.error('Please call stopRecording before starting recording.'); + return; + } + if (this.avRecorder.state !== AVRecorderStateEnum.PREPARED) { + this.logger.error('Please call prepareRecording before starting recording.'); + return; + } + await this.avRecorder.start(); + this.isRecording = true; + this.logger.info(`start recording.`); + this.logger.debug('start recording.'); + stopWatch.reset(); + stopWatch.start(); + this.startTimer(); + } catch (error) { + this.logger.error(`startRecording failed, code is ${error?.code}, message is ${error?.message}.`); + } + } + + //暂停录制 + public async pauseRecording() { + if (this.avRecorder.state === AVRecorderStateEnum.STARTED) { //仅在started状态下调用pause为合理状态切换 + try { + await this.avRecorder.pause(); + stopWatch.stop(); + this.logger.debug('pause recording.'); + } catch (error) { + this.logger.error(`pauseRecording failed, code is ${error?.code}, message is ${error?.message}.`); + } + } else { + this.logger.error('It is reasonable to call pauseRecording only in the started state.'); + return; + } + } + + //恢复录制 + public async resumeRecording() { + if (this.avRecorder.state === AVRecorderStateEnum.PAUSED) { //仅在paused状态下调用resume为合理状态切换 + try { + await this.avRecorder.resume(); + stopWatch.start(); + this.logger.debug('resume recording.'); + } catch (error) { + this.logger.error(`resumeRecording failed. code is ${error?.code}, message is ${error?.message}.`); + } + } else { + this.logger.error('It is reasonable to call resumeRecording only in the paused state.'); + return; + } + } + + //停止录制 + public async stopRecording() { + if (this.avRecorder.state === AVRecorderStateEnum.STARTED || this.avRecorder.state === AVRecorderStateEnum.PAUSED) { + try { + //仅在started或者paused状态下调用stop为合理状态切换 + + await this.avRecorder.stop(); + //重置 + await this.avRecorder.reset(); + //释放录制实例 + await this.avRecorder.release(); + this.isRecording = false; + this.stopTimer(); + stopWatch.stop(); + } catch (error) { + this.logger.error(`stopRecording failed. code is ${error?.code}, message is ${error?.message}.`); + } finally { + fs.closeSync(this.file); + this.convertM4aToBase64(); + this.logger.debug('stop recording.'); + } + } else { + this.logger.error('It is reasonable to call stopRecording only in the started or paused state.'); + return; + } + } + + //将音频文件转成base64格式 + convertM4aToBase64() { + let currentTime: number = stopWatch.getTimeSeconds(); + let base64: string = ''; + let stat: fs.Stat = fs.lstatSync(this.filePath); + let flag: boolean = true; + if (this.includeBase64) { + let file: fs.File = fs.openSync(this.filePath, fs.OpenMode.READ_ONLY); + try { + let buffer = new ArrayBuffer(stat.size); + fs.readSync(file.fd, buffer); + let unit8Array: Uint8Array = new Uint8Array(buffer); + let base64Helper = new util.Base64Helper(); + base64 = base64Helper.encodeToStringSync(unit8Array, util.Type.BASIC); + } catch (error) { + flag = false; + this.logger.error(`base64Helper encodeToString failed. code is ${error?.code}, message is ${error?.message}.`); + } finally { + fs.closeSync(file); + } + } + this.ctx.rnInstance.emitDeviceEvent('recordingFinished', { + base64, + duration: currentTime, + status: flag ? 'OK' : 'ERROR', + audioFileURL: this.filePath, + audioFileSize: stat.size + }); + } + + //获取存储路径 + public getAllPath(): PathMap { + const pathMap: PathMap = { + FilesDirectoryPath: this.context.filesDir, + CacheDirectoryPath: this.context.cacheDir, + TempsDirectoryPath: this.context.tempDir, + } + this.logger.info(`return the pathMap.`); + return pathMap; + } + + private startTimer() { + this.stopTimer(); + this.timer = setInterval(() => { + if (this.avRecorder.state === AVRecorderStateEnum.STARTED) { + let currentTime: number = stopWatch.getTimeSeconds(); + this.ctx.rnInstance.emitDeviceEvent('recordingProgress', { currentTime }); + } + }, 1000) + } + + private stopTimer() { + if (this.timer !== null) { + clearInterval(this.timer); + this.timer = null; + } + } +} \ No newline at end of file diff --git a/harmony/audio/src/main/ets/AudioType.ts b/harmony/audio/src/main/ets/AudioType.ts new file mode 100644 index 0000000..8732512 --- /dev/null +++ b/harmony/audio/src/main/ets/AudioType.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +type AudioQuality = 'Low' | 'Medium' | 'High'; + +export interface RecordingOptions { + SampleRate: number, + Channels: number, + AudioQuality?: AudioQuality, + AudioEncoding: string, + MeteringEnabled?: boolean, + MeasurementMode?: boolean, + AudioEncodingBitRate: number, + IncludeBase64: boolean, + OutputFormat: string, + AudioSource: number +} + +export interface PathMap { + FilesDirectoryPath: string, + CacheDirectoryPath: string, + TempsDirectoryPath: string, +} diff --git a/harmony/audio/src/main/ets/StopWatch.ts b/harmony/audio/src/main/ets/StopWatch.ts new file mode 100644 index 0000000..8cd0dc8 --- /dev/null +++ b/harmony/audio/src/main/ets/StopWatch.ts @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ + +class StopWatch { + private startTime: number = 0; + private elapsedTime: number = 0; + private paused: boolean = true; + + public start(): void { + this.startTime = new Date().getTime(); + this.paused = false; + } + + public stop(): number { + if (!this.paused) { + let nowTime = new Date().getTime(); + this.elapsedTime += (nowTime - this.startTime) / 1000; + this.paused = true; + } + return this.elapsedTime; + } + + public reset(): void { + this.startTime = 0; + this.elapsedTime = 0; + this.paused = true; + } + + public getTimeSeconds(): number { + let seconds: number = 0; + if (this.paused) { + seconds = this.elapsedTime; + } else { + let nowTime = new Date().getTime(); + seconds = this.elapsedTime + (nowTime - this.startTime) / 1000; + } + return seconds; + } +} + +export const stopWatch = new StopWatch(); \ No newline at end of file diff --git a/harmony/audio/src/main/ets/generated/components/ts.ts b/harmony/audio/src/main/ets/generated/components/ts.ts new file mode 100644 index 0000000..d1dae56 --- /dev/null +++ b/harmony/audio/src/main/ets/generated/components/ts.ts @@ -0,0 +1,5 @@ + +/** + */ + +export {} diff --git a/harmony/audio/src/main/ets/generated/index.ets b/harmony/audio/src/main/ets/generated/index.ets new file mode 100644 index 0000000..041b7ed --- /dev/null +++ b/harmony/audio/src/main/ets/generated/index.ets @@ -0,0 +1,5 @@ +/** + * This code was generated by "react-native codegen-lib-harmony" + */ + +export * from "./ts" diff --git a/harmony/audio/src/main/ets/generated/ts.ts b/harmony/audio/src/main/ets/generated/ts.ts new file mode 100644 index 0000000..4c568a8 --- /dev/null +++ b/harmony/audio/src/main/ets/generated/ts.ts @@ -0,0 +1,6 @@ +/** + * This code was generated by "react-native codegen-lib-harmony" + */ + +export * as RNC from "./components/ts" +export * as TM from "./turboModules/ts" diff --git a/harmony/audio/src/main/ets/generated/turboModules/RTNAudio.ts b/harmony/audio/src/main/ets/generated/turboModules/RTNAudio.ts new file mode 100644 index 0000000..3741eb4 --- /dev/null +++ b/harmony/audio/src/main/ets/generated/turboModules/RTNAudio.ts @@ -0,0 +1,32 @@ +/** + * This code was generated by "react-native codegen-lib-harmony" + */ + +import { Tag } from "@rnoh/react-native-openharmony/ts" + +export namespace RTNAudio { + export const NAME = 'RTNAudio' as const + + export type RecordingOptions = {SampleRate: number, Channels: number, AudioQuality?: string, AudioEncoding: string, MeteringEnabled?: boolean, MeasurementMode?: boolean, AudioEncodingBitRate: number, IncludeBase64: boolean, OutputFormat: string, AudioSource: number} + + export type PathMap = {FilesDirectoryPath: string, CacheDirectoryPath: string, TempsDirectoryPath: string} + + export interface Spec { + prepareRecordingAtPath(path: string, options: RecordingOptions): Promise; + + requestAuthorization(): Promise; + + startRecording(): Promise; + + pauseRecording(): Promise; + + resumeRecording(): Promise; + + stopRecording(): Promise; + + getAllPath(): PathMap; + + checkAuthorizationStatus(): Promise; + + } +} diff --git a/harmony/audio/src/main/ets/generated/turboModules/ts.ts b/harmony/audio/src/main/ets/generated/turboModules/ts.ts new file mode 100644 index 0000000..0a7017f --- /dev/null +++ b/harmony/audio/src/main/ets/generated/turboModules/ts.ts @@ -0,0 +1,5 @@ +/** + * This code was generated by "react-native codegen-lib-harmony" + */ + +export * from "./RTNAudio" diff --git a/harmony/audio/src/main/module.json5 b/harmony/audio/src/main/module.json5 new file mode 100644 index 0000000..1fe6484 --- /dev/null +++ b/harmony/audio/src/main/module.json5 @@ -0,0 +1,19 @@ +{ + module: { + name: 'audio', + type: 'har', + deviceTypes: ['default'], + mainElement: "AudioRecorder", + requestPermissions: [ + { + name: 'ohos.permission.MICROPHONE', + reason: "$string:label_permission_microphone", + usedScene: { + abilities: [ + "EntryAbility" + ], + }, + } + ] + }, +} diff --git a/harmony/audio/src/main/resources/base/element/string.json b/harmony/audio/src/main/resources/base/element/string.json new file mode 100644 index 0000000..b95c1da --- /dev/null +++ b/harmony/audio/src/main/resources/base/element/string.json @@ -0,0 +1,20 @@ +{ + "string": [ + { + "name": "page_show", + "value": "page from npm package" + }, + { + "name": "label_permission_write_media", + "value": "用于媒体文件写" + }, + { + "name": "label_permission_read_media", + "value": "用于媒体文件读" + }, + { + "name": "label_permission_microphone", + "value": "用于录制音频" + } + ] +} diff --git a/harmony/audio/src/main/resources/en_US/element/string.json b/harmony/audio/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000..1e76de0 --- /dev/null +++ b/harmony/audio/src/main/resources/en_US/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "page_show", + "value": "page from npm package" + } + ] +} diff --git a/harmony/audio/src/main/resources/zh_CN/element/string.json b/harmony/audio/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000..1e76de0 --- /dev/null +++ b/harmony/audio/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "page_show", + "value": "page from npm package" + } + ] +} diff --git a/harmony/audio/ts.ts b/harmony/audio/ts.ts new file mode 100644 index 0000000..a826d0d --- /dev/null +++ b/harmony/audio/ts.ts @@ -0,0 +1,7 @@ +/* + * Copyright (c) 2024 Huawei Device Co., Ltd. All rights reserved + * Use of this source code is governed by a MIT license that can be + * found in the LICENSE file. + */ +export * from "./src/main/ets/AudioPackage"; +export * from "./src/main/ets/AudioModule"; \ No newline at end of file diff --git a/index.js b/index.js index 44f1bff..664514d 100644 --- a/index.js +++ b/index.js @@ -4,18 +4,22 @@ import React from "react"; import ReactNative, { NativeModules, - NativeAppEventEmitter, DeviceEventEmitter, PermissionsAndroid, Platform } from "react-native"; -var AudioRecorderManager = NativeModules.AudioRecorderManager; +// @ts-ignore We want to check whether __turboModuleProxy exitst, it may not +const isTurboModuleEnabled = global.__turboModuleProxy != null; -var AudioRecorder = { - prepareRecordingAtPath: function(path, options) { +const nativeRecorderManager = isTurboModuleEnabled ? + require("./NativeAudio").default : + NativeModules.AudioRecorderManager; + +const AudioRecorder = { + prepareRecordingAtPath: function (path, options) { if (this.progressSubscription) this.progressSubscription.remove(); - this.progressSubscription = NativeAppEventEmitter.addListener('recordingProgress', + this.progressSubscription = DeviceEventEmitter.addListener('recordingProgress', (data) => { if (this.onProgress) { this.onProgress(data); @@ -24,7 +28,7 @@ var AudioRecorder = { ); if (this.finishedSubscription) this.finishedSubscription.remove(); - this.finishedSubscription = NativeAppEventEmitter.addListener('recordingFinished', + this.finishedSubscription = DeviceEventEmitter.addListener('recordingFinished', (data) => { if (this.onFinished) { this.onFinished(data); @@ -32,23 +36,23 @@ var AudioRecorder = { } ); - var defaultOptions = { - SampleRate: 44100.0, + const defaultOptions = { + SampleRate: 48000, Channels: 2, AudioQuality: 'High', AudioEncoding: 'ima4', OutputFormat: 'mpeg_4', MeteringEnabled: false, MeasurementMode: false, - AudioEncodingBitRate: 32000, + AudioEncodingBitRate: 100000, IncludeBase64: false, AudioSource: 0 }; - var recordingOptions = {...defaultOptions, ...options}; + const recordingOptions = { ...defaultOptions, ...options }; if (Platform.OS === 'ios') { - AudioRecorderManager.prepareRecordingAtPath( + nativeRecorderManager.prepareRecordingAtPath( path, recordingOptions.SampleRate, recordingOptions.Channels, @@ -59,41 +63,62 @@ var AudioRecorder = { recordingOptions.IncludeBase64 ); } else { - return AudioRecorderManager.prepareRecordingAtPath(path, recordingOptions); + return nativeRecorderManager.prepareRecordingAtPath(path, recordingOptions); } }, - startRecording: function() { - return AudioRecorderManager.startRecording(); + startRecording: function () { + return nativeRecorderManager.startRecording(); }, - pauseRecording: function() { - return AudioRecorderManager.pauseRecording(); + pauseRecording: function () { + return nativeRecorderManager.pauseRecording(); }, - resumeRecording: function() { - return AudioRecorderManager.resumeRecording(); + resumeRecording: function () { + return nativeRecorderManager.resumeRecording(); }, - stopRecording: function() { - return AudioRecorderManager.stopRecording(); + stopRecording: async function () { + await nativeRecorderManager.stopRecording(); + const timer = setTimeout( ()=>{ + this.removeListeners(); + this.clearCallback(); + clearTimeout(timer); + }, 200 ); }, - checkAuthorizationStatus: AudioRecorderManager.checkAuthorizationStatus, - requestAuthorization: () => { - if (Platform.OS === 'ios') - return AudioRecorderManager.requestAuthorization(); - else + checkAuthorizationStatus: nativeRecorderManager.checkAuthorizationStatus, + requestAuthorization: async () => { + if (Platform.OS === 'harmony') { + const res = await nativeRecorderManager.requestAuthorization(); + return res; + } else { return new Promise((resolve, reject) => { PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.RECORD_AUDIO ).then(result => { - if (result == PermissionsAndroid.RESULTS.GRANTED || result == true) + if (result === PermissionsAndroid.RESULTS.GRANTED || result === true) { resolve(true); - else - resolve(false) - }) + } + else { + resolve(false); + } + }); }); + } }, - removeListeners: function() { + getAllPath: function () { + const res = nativeRecorderManager.getAllPath(); + return res; + }, + removeListeners: function () { if (this.progressSubscription) this.progressSubscription.remove(); if (this.finishedSubscription) this.finishedSubscription.remove(); }, + clearCallback: function () { + if (this.onProgress) { + this.onProgress = null; + } + if (this.onFinished) { + this.onFinished = null; + } + } }; let AudioUtils = {}; @@ -101,20 +126,31 @@ let AudioSource = {}; if (Platform.OS === 'ios') { AudioUtils = { - MainBundlePath: AudioRecorderManager.MainBundlePath, - CachesDirectoryPath: AudioRecorderManager.NSCachesDirectoryPath, - DocumentDirectoryPath: AudioRecorderManager.NSDocumentDirectoryPath, - LibraryDirectoryPath: AudioRecorderManager.NSLibraryDirectoryPath, + MainBundlePath: nativeRecorderManager.MainBundlePath, + CachesDirectoryPath: nativeRecorderManager.NSCachesDirectoryPath, + DocumentDirectoryPath: nativeRecorderManager.NSDocumentDirectoryPath, + LibraryDirectoryPath: nativeRecorderManager.NSLibraryDirectoryPath, + }; +} else if (Platform.OS === 'harmony') { + const { FilesDirectoryPath, CacheDirectoryPath, TempsDirectoryPath } = AudioRecorder.getAllPath(); + AudioUtils = { + FilesDirectoryPath, + CacheDirectoryPath, + TempsDirectoryPath + }; + AudioSource = { + DEFAULT: 0, + MIC: 1, }; } else if (Platform.OS === 'android') { AudioUtils = { - MainBundlePath: AudioRecorderManager.MainBundlePath, - CachesDirectoryPath: AudioRecorderManager.CachesDirectoryPath, - DocumentDirectoryPath: AudioRecorderManager.DocumentDirectoryPath, - LibraryDirectoryPath: AudioRecorderManager.LibraryDirectoryPath, - PicturesDirectoryPath: AudioRecorderManager.PicturesDirectoryPath, - MusicDirectoryPath: AudioRecorderManager.MusicDirectoryPath, - DownloadsDirectoryPath: AudioRecorderManager.DownloadsDirectoryPath + MainBundlePath: nativeRecorderManager.MainBundlePath, + CachesDirectoryPath: nativeRecorderManager.CachesDirectoryPath, + DocumentDirectoryPath: nativeRecorderManager.DocumentDirectoryPath, + LibraryDirectoryPath: nativeRecorderManager.LibraryDirectoryPath, + PicturesDirectoryPath: nativeRecorderManager.PicturesDirectoryPath, + MusicDirectoryPath: nativeRecorderManager.MusicDirectoryPath, + DownloadsDirectoryPath: nativeRecorderManager.DownloadsDirectoryPath }; AudioSource = { DEFAULT: 0, @@ -130,4 +166,4 @@ if (Platform.OS === 'ios') { }; } -module.exports = {AudioRecorder, AudioUtils, AudioSource}; +module.exports = { AudioRecorder, AudioUtils, AudioSource }; diff --git a/package.json b/package.json index 9b975d6..4bbb13f 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { - "name": "react-native-audio", - "version": "4.2.2", + "name": "@react-native-ohos/react-native-audio", + "version": "4.2.3-rc1", "description": "React Native extension for recording audio", "main": "index.js", "author": "Joshua Sierles (https://github.com/jsierles)", + "harmony": { + "alias": "react-native-audio" + }, "files": [ - "ios/AudioRecorderManager.m", - "ios/AudioRecorderManager.h", - "ios/RNAudio.xcodeproj", + "/harmony", "README.md", "LICENSE", "index.js", - "android/*", - "RNAudio.podspec" + "NativeAudio.ts" ], "keywords": [ "react-native", @@ -21,7 +21,17 @@ ], "repository": { "type": "git", - "url": "git@github.com:jsierles/react-native-audio.git" + "url": "https://gitee.com/openharmony-sig/rntpc_react-native-audio" + }, + "scripts": { + "codegen-lib": "react-native codegen-lib-harmony --no-safety-check --npm-package-name react-native-audio --cpp-output-path ./harmony/audio/src/main/cpp/generated --ets-output-path ./harmony/audio/src/main/ets/generated --turbo-modules-spec-paths ./NativeAudio.ts " + }, + "devDependencies": { + "@rnoh/react-native-harmony-cli": "npm:@react-native-oh/react-native-harmony-cli@^0.0.27" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org/", + "access": "public" }, "nativePackage": true -} +} \ No newline at end of file -- Gitee