模板版本:v0.2.2
[!TIP] Github 地址
请到三方库的 Releases 发布地址查看配套的版本信息:@react-native-oh-tpl/audio-toolkit Releases 。对于未发布到npm的旧版本,请参考安装指南安装tgz包。
进入到工程目录并输入以下命令:
npm install @react-native-oh-tpl/audio-toolkit
yarn add @react-native-oh-tpl/audio-toolkit
下面的代码展示了这个库的基本使用场景:
[!WARNING] 使用时 import 的库名不变。
[!TIP] # demo使用了三方库@react-native-oh-tpl/react-native-slider
[!TIP] # demo使用了权限三方库@react-native-oh-tpl/react-native-permissions
import React, { Component } from 'react';
import { Button, PermissionsAndroid, Platform, StyleSheet, Switch, Text, View, ScrollView } from 'react-native';
import Slider from '@react-native-oh-tpl/react-native-slider';
import { Player, Recorder, MediaStates } from '@react-native-community/audio-toolkit';
import RTNPermissions, {RESULTS} from "@react-native-oh-tpl/react-native-permissions";
const filename = 'test.mp4';
type Props = {};
type State = {
playPauseButton: string,
recordButton: string,
stopButtonDisabled: boolean,
playButtonDisabled: boolean | undefined,
recordButtonDisabled: boolean,
recordPauseButtonDisabled: boolean,
loopButtonStatus: boolean,
progress: number,
error: string | null
};
export default class App extends Component<Props, State> {
player: Player | null = null;
recorder: Recorder | null = null;
lastSeek: number = 0;
_progressInterval: NodeJS.Timeout | null = null;
constructor(props: Props) {
super(props);
this.state = {
playPauseButton: 'Preparing...',
recordButton: 'Preparing...',
stopButtonDisabled: true,
playButtonDisabled: true,
recordButtonDisabled: true,
recordPauseButtonDisabled:true,
loopButtonStatus: false,
progress: 0,
error: null
};
}
componentWillMount() {
this.player = null;
this.recorder = null;
this.lastSeek = 0;
this._reloadPlayer();
this._reloadRecorder();
this._progressInterval = setInterval(() => {
if (this.player && this._shouldUpdateProgressBar()) {
let currentProgress = Math.max(0, this.player.currentTime) / this.player.duration;
if (isNaN(currentProgress)) {
currentProgress = 0;
}
this.setState({ progress: currentProgress });
}
}, 100);
}
componentWillUnmount() {
if (this._progressInterval) {
clearInterval(this._progressInterval);
}
}
_shouldUpdateProgressBar() {
// Debounce progress bar update by 200 ms
return Date.now() - this.lastSeek > 200;
}
_updateState() {
this.setState({
playPauseButton: this.player && this.player.isPlaying ? 'Pause' : 'Play',
recordButton: this.recorder && this.recorder.isRecording ? 'Stop' : 'Record',
recordPauseButtonDisabled: !this.recorder || !this.recorder.isRecording,
stopButtonDisabled: !this.player || !this.player.canStop,
playButtonDisabled: !this.player || !this.player.canPlay || this.recorder?.isRecording,
recordButtonDisabled: (!this.recorder || (this.player && !this.player.isStopped)) ?? true,
});
}
_playPause() {
this.player?.playPause((err, paused) => {
if (err) {
this.setState({
error: err.message
});
}
this._updateState();
});
}
_stop() {
this.player?.stop(() => {
this._updateState();
});
}
_seek(percentage: number) {
if (!this.player) {
return;
}
this.lastSeek = Date.now();
let position = percentage * this.player.duration;
this.player.seek(position, () => { });
}
_getPlayPath() {
if (this.recorder) {
return this.recorder.fsPath;
}
return filename;
}
_reloadPlayer() {
if (this.player) {
this.player.destroy();
}
this.player = new Player(this._getPlayPath(), {
autoDestroy: false
}).prepare((err) => {
if (err) {
console.log('error at _reloadPlayer():');
console.log(err);
} else {
if (this.player) {
this.player.looping = this.state.loopButtonStatus;
}
}
this._updateState();
});
this._updateState();
this.player.on('ended', () => {
this._updateState();
});
this.player.on('pause', () => {
this._updateState();
});
}
_reloadRecorder() {
if (this.recorder) {
this.recorder.destroy();
}
this.recorder = new Recorder(filename, {
bitrate: 256000,
channels: 2,
sampleRate: 44100,
quality: 'max'
});
this._updateState();
}
_toggleRecord() {
if(this.recorder && this.recorder.state === MediaStates.PAUSED){
this.recorder?.record((err) => {
this._updateState();
})
return
}
if (this.player) {
this.player.destroy();
}
let recordAudioRequest;
if (Platform.OS == 'android') {
recordAudioRequest = this._requestRecordAudioPermission();
} else if (Platform.OS === 'harmony') {
recordAudioRequest = this._requestRecordAudioPermissionHs();
} else {
recordAudioRequest = new Promise(function (resolve, reject) { resolve(true); });
}
recordAudioRequest.then((hasPermission) => {
if (!hasPermission) {
this.setState({
error: 'Record Audio Permission was denied'
});
return;
}
this.recorder?.toggleRecord((err, stopped) => {
if (err) {
this.setState({
error: err.message
});
}
if (stopped) {
this._reloadPlayer();
this._reloadRecorder();
}
this._updateState();
});
});
}
async _requestRecordAudioPermissionHs() {
try {
let check = await RTNPermissions.request('ohos.permission.MICROPHONE');
console.info("RTNPermissions===== request", check);
if (check === RESULTS.GRANTED) {
return true;
} else {
return false;
}
} catch (err) {
console.error(err);
return false;
}
}
async _requestRecordAudioPermission() {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
{
title: 'Microphone Permission',
message: 'ExampleApp needs access to your microphone to test react-native-audio-toolkit.',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
},
);
if (granted === PermissionsAndroid.RESULTS.GRANTED) {
return true;
} else {
return false;
}
} catch (err) {
console.error(err);
return false;
}
}
_toggleLooping(value: boolean) {
this.setState({
loopButtonStatus: value
});
if (this.player) {
this.player.looping = value;
}
}
render() {
return (
<ScrollView>
<View>
<Text style={styles.title}>
Playback
</Text>
</View>
<View >
<Button title={this.state.playPauseButton} disabled={this.state.playButtonDisabled} onPress={() => this._playPause()} />
<Button title={'Stop'} disabled={this.state.stopButtonDisabled} onPress={() => this._stop()} />
</View>
<View style={styles.settingsContainer}>
<Switch
onValueChange={(value) => this._toggleLooping(value)}
value={this.state.loopButtonStatus} />
<Text>Toggle Looping</Text>
</View>
<View style={styles.slider}>
<Slider step={0.0001} disabled={this.state.playButtonDisabled} onValueChange={(percentage) => this._seek(percentage)} value={this.state.progress} />
</View>
<View>
<Text style={styles.title}>
Recording
</Text>
</View>
<View>
<Button title={this.state.recordButton} disabled={this.state.recordButtonDisabled} onPress={() => this._toggleRecord()} />
</View>
<View style={styles.recordPause}>
<Button title={'Pause recording'} disabled={this.state.recordPauseButtonDisabled} onPress={() => {
this.recorder?.pause((err) => {
this._updateState();
})
}} />
</View>
<View>
<Text style={styles.errorMessage}>{this.state.error}</Text>
</View>
</ScrollView>
);
}
}
const styles = StyleSheet.create({
slider: {
height: 10,
margin: 10,
marginBottom: 30,
},
settingsContainer: {
alignItems: 'center',
},
container: {
borderRadius: 4,
borderWidth: 0.5,
borderColor: '#d6d7da',
},
title: {
fontSize: 19,
fontWeight: 'bold',
textAlign: 'center',
padding: 20,
},
errorMessage: {
fontSize: 15,
textAlign: 'center',
padding: 10,
color: 'red'
},
recordPause:{
marginTop:15
}
});
本库已经适配了 Codegen
,在使用前需要主动执行生成三方库桥接代码,详细请参考 Codegen 使用文档。
目前 HarmonyOS 暂不支持 AutoLink,所以 Link 步骤需要手动配置。
首先需要使用 DevEco Studio 打开项目里的 HarmonyOS 工程 harmony
{
...
"overrides": {
"@rnoh/react-native-openharmony" : "./react_native_openharmony"
}
}
目前有两种方法:
- 通过 har 包引入(在 IDE 完善相关功能后该方法会被遗弃,目前首选此方法);
- 直接链接源码。
方法一:通过 har 包引入(推荐)
[!TIP] har 包位于三方库安装路径的
harmony
文件夹下。
打开 entry/oh-package.json5
,添加以下依赖
"dependencies": {
"@rnoh/react-native-openharmony": "file:../react_native_openharmony",
"@react-native-oh-tpl/audio-toolkit": "file:../../node_modules/@react-native-oh-tpl/audio-toolkit/harmony/audio_toolkit.har"
}
打开 entry/src/main/ets/RNPackagesFactory.ts
,添加:
...
+ import { AudioModulesPackage } from "@react-native-oh-tpl/audio-toolkit/ts";
export function createRNPackages(ctx: RNPackageContext): RNPackage[] {
return [
new SamplePackage(ctx),
+ new AudioModulesPackage(ctx)
];
}
点击右上角的 sync
按钮
或者在终端执行:
cd entry
ohpm install
方法二:直接链接源码
[!TIP] 如需使用直接链接源码,请参考直接链接源码说明
点击右上角的 sync
按钮
或者在终端执行:
cd entry
ohpm install
然后编译、运行即可。
要使用此库,需要使用正确的 React-Native 和 RNOH 版本。另外,还需要使用配套的 DevEco Studio 和 手机 ROM。
请到三方库相应的 Releases 发布地址查看 Release 配套的版本信息:@react-native-oh-tpl/audio-toolkit Releases
-
如果 source 使用网络 url 应用需要申请网络权限
在
entry/src/main/module.json5
中添加
requestPermissions: [
{
name: "ohos.permission.INTERNET",
},
],
-
如果使用录音功能,需要申请录音权限
在
entry/src/main/module.json5
中添加
requestPermissions: [
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:EntryAbility_desc",
"usedScene": {
"abilities": [
"MainAbility"
],
"when": "inuse"
}
},
],
[!TIP] "Platform"列表示该属性在原三方库上支持的平台。
[!TIP] "HarmonyOS Support"列为 yes 表示 HarmonyOS 平台支持该属性;no 则表示不支持;partially 表示部分支持。使用方法跨平台一致,效果对标 iOS 或 Android 的效果。
Name | Description | Type | Default | Required | Platform | HarmonyOS Support |
---|---|---|---|---|---|---|
autoDestroy | Boolean to indicate whether the player should self-destruct after playback is finished. If this is not set, you are responsible for destroying the object by calling player.destroy(). | boolean | True | No | All | Yes |
continuesToPlayInBackground | (Android only) Should playback continue if app is sent to background? iOS will always pause in this case. |
boolean | False | No | Android | Yes |
category | (iOS only) Define the audio session category Options: Playback, Ambient and SoloAmbient More info about categories can be found here: https://developer.apple.com/documentation/avfoundation/avaudiosession/category |
PlaybackCategories | PlaybackCategories.Playback | No | IOS | No |
mixWithOthers | Boolean to determine whether other audio sources on the device will mix with sounds being played back by this module. If this is not set, playback of audio will stop other sources |
boolean | False | No | Android IOS |
No |
looping | Get/set looping status of the current file. If true, file will loop when playback reaches end of file. | boolean | False | No | All | Yes |
volume | Get/set playback volume. The scale is from 0.0 (silence) to 1.0 (full volume). | Number | 1.0 | No | All | Yes |
speed | Get/set the playback speed for audio. NOTE: On Android, this is only supported on Android 6.0+. | Number | 1.0 | No | All | Yes |
[!TIP] "Platform"列表示该属性在原三方库上支持的平台。
[!TIP] "HarmonyOS Support"列为 yes 表示 HarmonyOS 平台支持该属性;no 则表示不支持;partially 表示部分支持。使用方法跨平台一致,效果对标 iOS 或 Android 的效果。
Name | Description | Type | Required | Platform | HarmonyOS Support |
---|---|---|---|---|---|
prepare | Prepare playback of the file provided during initialization. This method is optional to call but might be useful to preload the file so that the file starts playing immediately when calling play(). Otherwise the file is prepared when calling play() which may result in a small delay. Callback is called with empty first parameter when file is ready for playback with play() . If there was an error, the callback is called with an error object as first parameter. See Callbacks for more information. |
function | No | All | Yes |
play | Start playback. If callback is given, it is called when playback has started. |
function | No | All | Yes |
pause | Pauses playback. Playback can be resumed by calling play() with no parameters.Callback is called after the operation has finished. |
function | No | All | Yes |
playPause | Helper method for toggling pause. Callback is called after the operation has finished. Callback receives Object error as first argument, Boolean paused as second argument indicating if the player ended up playing (false) or paused (true). |
function | No | All | Yes |
stop | Stop playback. If autoDestroy option was set during initialization, clears all media resources from memory. In this case the player should no longer be used. |
function | No | All | Yes |
destroy | Stops playback and destroys the player. The player should no longer be used. Callback is called after the operation has finished. |
function | No | All | Yes |
seek | Seek in currently playing media. position is the offset from the start.If callback is given, it is called when the seek operation completes. If another seek operation is performed before the previous has finished, the previous operation gets an error in its callback with the err field set to oldcallback . The previous operation should likely do nothing in this case. |
function | No | All | Yes |
[!TIP] "Platform"列表示该属性在原三方库上支持的平台。
[!TIP] "HarmonyOS Support"列为 yes 表示 HarmonyOS 平台支持该属性;no 则表示不支持;partially 表示部分支持。使用方法跨平台一致,效果对标 iOS 或 Android 的效果。
Name | Description | Type | Default | Required | Platform | HarmonyOS Support |
---|---|---|---|---|---|---|
bitrate | Set bitrate for the recorder, in bits per second | Number | 128000 | No | All | Yes |
channels | Set number of channels | Number | 2 | No | All | Yes |
sampleRate | Set how many samples per second | Number | 48000 | No | All | Yes |
format | Override format. Possible values: Cross-platform: 'mp4' Android only: 'ogg', 'webm', 'amr' HarmonyOS only: 'm4a' |
String | based on filename extension | No | All | Yes |
encoder | Override encoder. Android only. Possible values: 'aac', 'mp4', 'webm', 'ogg', 'amr' HarmonyOS only: 'aac' |
String | based on filename extension | No | Android | partially |
quality | Quality of the recording, iOS only. Possible values: 'min', 'low', 'medium', 'high', 'max' |
String | medium | No | IOS | No |
[!TIP] "Platform"列表示该属性在原三方库上支持的平台。
[!TIP] "HarmonyOS Support"列为 yes 表示 HarmonyOS 平台支持该属性;no 则表示不支持;partially 表示部分支持。使用方法跨平台一致,效果对标 iOS 或 Android 的效果。
Name | Description | Type | Required | Platform | HarmonyOS Support |
---|---|---|---|---|---|
prepare | Prepare recording to the file provided during initialization. This method is optional to call but it may be beneficial to call to make sure that recording begins immediately after calling record(). Otherwise the recording is prepared when calling record() which may result in a small delay. NOTE: Assume that this wipes the destination file immediately. | function | No | All | Yes |
record | Start recording to file in path . Callback is called after recording has started or with error object if an error occurred. |
function | No | All | Yes |
stop | Stop recording and save the file. Callback is called after recording has stopped or with error object. The recorder is destroyed after calling stop and should no longer be used. | function | No | All | Yes |
destroy | Destroy the recorder. Should only be used if a recorder was constructed, and for some reason is now unwanted. Callback is called after the operation has finished. |
function | No | All | Yes |
var MediaStates = {
DESTROYED: -2,
ERROR: -1,
IDLE: 0,
PREPARING: 1,
PREPARED: 2,
SEEKING: 3,
PLAYING: 4, // only for Player
RECORDING: 4, // only for Recorder
PAUSED: 5
};
- Recorder的encoder属性问题:目前仅支持aac,待后续底层能力开放增加其他格式。issue#15
-
Player的category、mixWithOthers属性 HarmonyOS不支持
-
Recorder的quality属性 HarmonyOS不支持
本项目基于 The MIT License (MIT) ,请自由地享受和参与开源。