提交 e5828f15 编写于 作者: Z zengyawen

update docs

Signed-off-by: Nzengyawen <zengyawen1@huawei.com>
上级 d57dbc67
# 文件管理
- 媒体库管理
- [媒体库开发概述](medialibrary-overview.md)
- [媒体资源使用指导](medialibrary-resource-guidelines.md)
- [文件路径使用指导](medialibrary-filepath-guidelines.md)
- [相册资源使用指导](medialibrary-album-guidelines.md)
# 文件
- 文件访问框架
- [用户公共文件访问框架概述](file-access-framework-overview.md)
- [文件选择器使用指导](filepicker-guidelines.md)
\ No newline at end of file
- [文件管理概述](file-management-overview.md)
- 应用文件
- [应用文件概述](app-file-overview.md)
- [应用沙箱目录](app-sandbox-directory.md)
- 应用文件访问与管理
- [应用文件访问](app-file-access.md)
- [应用文件上传下载](app-file-upload-download.md)
- [应用及文件系统空间统计](app-fs-space-statistics.md)
- [向应用沙箱推送文件](send-file-to-app-sandbox.md)
- [应用文件分享](share-app-file.md)
- 用户文件
- [用户文件概述](user-file-overview.md)
- 选择与保存用户文件(FilePicker)
- [选择用户文件](select-user-file.md)
- [保存用户文件](save-user-file.md)
- [开发用户文件管理器(仅对系统应用开放)](dev-user-file-manager.md)
- [管理外置存储设备(仅对系统应用开放)](manage-external-storage.md)
- 分布式文件系统
- [分布式文件系统概述](distributed-fs-overview.md)
- [设置分布式文件数据等级](set-security-label.md)
- [跨设备文件访问](file-access-across-devices.md)
# 应用文件访问
应用需要对应用文件目录下的应用文件进行查看、创建、读写、删除、移动、复制、获取属性等访问操作,下文介绍具体方法。
## 接口说明
开发者通过基础文件操作接口([ohos.file.fs](../reference/apis/js-apis-file-fs.md))实现应用文件访问能力,主要功能如下表所示。
**表1** 基础文件操作接口功能
| 接口名 | 功能 | 接口类型 | 支持同步 | 支持异步 |
| -------- | -------- | -------- | -------- | -------- |
| access | 检查文件是否存在 | 方法 | √ | √ |
| close | 关闭文件 | 方法 | √ | √ |
| copyFile | 复制文件 | 方法 | √ | √ |
| createStream | 基于文件路径打开文件流 | 方法 | √ | √ |
| listFile | 列出文件夹下所有文件名 | 方法 | √ | √ |
| mkdir | 创建目录 | 方法 | √ | √ |
| moveFile | 移动文件 | 方法 | √ | √ |
| open | 打开文件 | 方法 | √ | √ |
| read | 从文件读取数据 | 方法 | √ | √ |
| rename | 重命名文件或文件夹 | 方法 | √ | √ |
| rmdir | 删除整个目录 | 方法 | √ | √ |
| stat | 获取文件详细属性信息 | 方法 | √ | √ |
| unlink | 删除单个文件 | 方法 | √ | √ |
| write | 将数据写入文件 | 方法 | √ | √ |
| Stream.close | 关闭文件流 | 方法 | √ | √ |
| Stream.flush | 刷新文件流 | 方法 | √ | √ |
| Stream.write | 将数据写入流文件 | 方法 | √ | √ |
| Stream.read | 从流文件读取数据 | 方法 | √ | √ |
| File.fd | 获取文件描述符 | 属性 | √ | × |
| OpenMode | 设置文件打开标签 | 属性 | √ | × |
| Filter | 设置文件过滤配置项 | 类型 | × | × |
## 开发示例
在对应用文件开始访问前,开发者需要[获取应用文件路径](../application-models/application-context-stage.md#获取应用开发路径)。以从UIAbilityContext获取HAP级别的文件路径为例进行说明,UIAbilityContext的获取方式请参见[获取UIAbility的上下文信息](../application-models/uiability-usage.md#获取uiability的上下文信息)
下面介绍几种常用操作示例。
### 新建并读写一个文件
以下示例代码演示了如何新建一个文件并对其读写。
```ts
// pages/xxx.ets
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
function createFile() {
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;
// 新建并打开文件
let file = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
// 写入一段内容至文件
let writeLen = fs.writeSync(file.fd, "Try to write str.");
console.info("The length of str is: " + writeLen);
// 从文件读取一段内容
let buf = new ArrayBuffer(1024);
let readLen = fs.readSync(file.fd, buf, { offset: 0 });
console.info("the content of file: " + String.fromCharCode.apply(null, new Uint8Array(buf.slice(0, readLen))));
// 关闭文件
fs.closeSync(file);
}
```
### 读取文件内容并写入到另一个文件
以下示例代码演示了如何从一个文件读写内容到另一个文件。
```ts
// pages/xxx.ets
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
function readWriteFile() {
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;
// 打开文件
let srcFile = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE);
let destFile = fs.openSync(filesDir + '/destFile.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
// 读取源文件内容并写入至目的文件
let bufSize = 4096;
let readSize = 0;
let buf = new ArrayBuffer(bufSize);
let readLen = fs.readSync(srcFile.fd, buf, { offset: readSize });
while (readLen > 0) {
readSize += readLen;
fs.writeSync(destFile.fd, buf);
readLen = fs.readSync(srcFile.fd, buf, { offset: readSize });
}
// 关闭文件
fs.closeSync(srcFile);
fs.closeSync(destFile);
}
```
> **说明:**
>
> 使用读写接口时,需注意可选项参数offset的设置。对于已存在且读写过的文件,文件偏移指针默认在上次读写操作的终止位置。
### 以流的形式读写文件
以下示例代码演示了如何使用流接口进行文件读写:
```ts
// pages/xxx.ets
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
async function readWriteFileWithStream() {
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;
// 打开文件流
let inputStream = fs.createStreamSync(filesDir + '/test.txt', 'r+');
let outputStream = fs.createStreamSync(filesDir + '/destFile.txt', "w+");
// 以流的形式读取源文件内容并写入目的文件
let bufSize = 4096;
let readSize = 0;
let buf = new ArrayBuffer(bufSize);
let readLen = await inputStream.read(buf, { offset: readSize });
readSize += readLen;
while (readLen > 0) {
await outputStream.write(buf);
readLen = await inputStream.read(buf, { offset: readSize });
readSize += readLen;
}
// 关闭文件流
inputStream.closeSync();
outputStream.closeSync();
}
```
> **说明:**
> 使用流接口时,需注意流的及时关闭。同时流的异步接口应严格遵循异步接口使用规范,避免同步、异步接口混用。流接口不支持并发读写。
### 查看文件列表
以下示例代码演示了如何查看文件列表:
```ts
// 查看文件列表
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;
// 查看文件列表
let options = {
recursion: false,
listNum: 0,
filter: {
suffix: ['.png', '.jpg', '.txt'], // 匹配文件后缀名为'.png','.jpg','.txt'
displayName: ['test%'], // 匹配文件全名以'test'开头
fileSizeOver: 0, // 匹配文件大小大于等于0
lastModifiedAfter: new Date(0).getTime(), // 匹配文件最近修改时间在1970年1月1日之后
},
}
let files = fs.listFileSync(filesDir, options);
for (let i = 0; i < files.length; i++) {
console.info(`The name of file: ${files[i]}`);
}
```
# 应用文件概述
应用文件:文件所有者为应用,包括应用安装文件、应用资源文件、应用缓存文件等。
- 设备上应用所使用及存储的数据,以文件、键值对、数据库等形式保存在一个应用专属的目录内。该专属目录我们称为“应用文件目录”,该目录下所有数据以不同的文件格式存放,这些文件即应用文件。
- “应用文件目录”与一部分系统文件(应用运行必须使用的系统文件)所在的目录组成了一个集合,该集合称为“[应用沙箱目录](app-sandbox-directory.md)”,代表应用可见的所有目录范围。因此“应用文件目录”是在“应用沙箱目录”内的。
- 系统文件及其目录对于应用是只读的;应用仅能保存文件到“[应用文件目录](app-sandbox-directory.md#应用文件目录与应用文件路径)”下,根据目录的使用规范和注意事项来选择将数据保存到不同的子目录中。
下文将详细介绍应用沙箱、应用文件目录、应用文件访问与管理、应用文件分享等相关内容。
# 应用文件上传下载
应用可以将应用文件上传到网络服务器,也可以从网络服务器下载网络资源文件到本地应用文件目录。
## 上传应用文件
开发者可以使用上传下载模块([ohos.request](../reference/apis/js-apis-request.md))的上传接口将本地文件上传。文件上传过程使用系统服务代理完成。
> **说明:**
> 当前上传应用文件功能,仅支持上传应用缓存文件路径(cacheDir)下的文件。
>
> 使用上传下载模块,需[申请相关权限](../security/accesstoken-guidelines.md):ohos.permission.INTERNET。
以下示例代码演示了如何将应用缓存文件路径下的文件上传至网络服务器。
```ts
// pages/xxx.ets
import common from '@ohos.app.ability.common';
import fs from '@ohos.file.fs';
import request from '@ohos.request';
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let cacheDir = context.cacheDir;
// 新建一个本地应用文件
let file = fs.openSync(cacheDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.writeSync(file.fd, 'upload file test');
fs.closeSync(file);
// 上传任务配置项
let uploadConfig = {
url: 'https://xxx',
header: { key1: 'value1', key2: 'value2' },
method: 'POST',
files: [
{ filename: 'test.txt', name: 'test', uri: 'internal://cache/test.txt', type: 'txt' }
],
data: [
{ name: 'name', value: 'value' }
]
}
// 将本地应用文件上传至网络服务器
try {
request.uploadFile(context, uploadConfig)
.then((uploadTask) => {
uploadTask.on('complete', (taskStates) => {
for (let i = 0; i < taskStates.length; i++) {
console.info(`upload complete taskState: ${JSON.stringify(taskStates[i])}');
}
});
})
.catch((err) => {
console.error(`Invoke uploadFile failed, code is ${err.code}, message is ${err.message}`);
})
} catch (err) {
console.error(`Invoke uploadFile failed, code is ${err.code}, message is ${err.message}`);
}
```
## 下载网络资源文件至应用文件目录
开发者可以使用上传下载模块([ohos.request](../reference/apis/js-apis-request.md))的下载接口将网络资源文件下载到应用文件目录。对已下载的网络资源文件,开发者可以使用基础文件IO接口([ohos.file.fs](../reference/apis/js-apis-file-fs.md))对其进行访问,使用方式与[应用文件访问](app-file-access.md)一致。文件下载过程使用系统服务代理完成。
> **说明:**
> 当前网络资源文件仅支持下载至应用文件目录。
>
> 使用上传下载模块,需[申请相关权限](../security/accesstoken-guidelines.md):ohos.permission.INTERNET。
以下示例代码演示了如何将网络资源文件下载到应用文件目录:
```
// pages/xxx.ets
// 将网络资源文件下载到应用文件目录并读取一段内容
import common from '@ohos.app.ability.common';
import fs from '@ohos.file.fs';
import request from '@ohos.request';
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir;
try {
request.downloadFile(context, {
url: 'https://xxxx/xxxx.txt',
filePath: filesDir + '/xxxx.txt'
}).then((downloadTask) => {
downloadTask.on('complete', () => {
console.info('download complete');
let file = fs.openSync(filesDir + '/xxxx.txt', fs.OpenMode.READ_WRITE);
let buf = new ArrayBuffer(1024);
let readLen = fs.readSync(file.fd, buf);
console.info(`The content of file: ${String.fromCharCode.apply(null, new Uint8Array(buf.slice(0, readLen)))}`);
fs.closeSync(file);
})
}).catch((err) => {
console.error(`Invoke downloadTask failed, code is ${err.code}, message is ${err.message}`);
});
} catch (err) {
console.error(`Invoke downloadFile failed, code is ${err.code}, message is ${err.message}`);
}
```
# 应用及文件系统空间统计
在系统中,可能出现系统空间不够或者cacheDir等目录受系统配额限制等情况,需要应用开发者关注系统剩余空间,同时控制应用自身占用的空间大小。
## 接口说明
API的详细介绍请参见[ohos.file.statvfs](../reference/apis/js-apis-file-statvfs.md)[ohos.file.storageStatistics](../reference/apis/js-apis-storage-statistics.md)
**表1** 文件系统空间和应用空间统计
| 模块 | 接口名 | 功能 |
| -------- | -------- | -------- |
| \@ohos.file.storageStatistic | getCurrentBundleStats | 获取当前应用的存储空间大小(单位为Byte)。 |
| \@ohos.file.statvfs | getFreeSize | 获取指定文件系统的剩余空间大小(单位为Byte)。 |
| \@ohos.file.statvfs | getTotalSize | 获取指定文件系统的总空间大小(单位为Byte)。 |
**表2** 应用空间统计
| BundleStats属性 | 含义 | 统计路径 |
| -------- | -------- | -------- |
| appSize | 应用安装文件大小(单位为Byte) | 应用安装文件保存在以下目录:<br/>/data/storage/el1/bundle |
| cacheSize | 应用缓存文件大小(单位为Byte) | 应用的缓存文件保存在以下目录:<br/>/data/storage/el1/base/cache<br/>/data/storage/el1/base/haps/entry/cache<br/>/data/storage/el2/base/cache<br/>/data/storage/el2/base/haps/entry/cache |
| dataSize | 应用文件存储大小(除应用安装文件和缓存文件)(单位为Byte) | 应用文件由本地文件、分布式文件以及数据库文件组成。<br/>本地文件保存在以下目录(注意缓存文件目录为以下目录的子目录):<br/>/data/storage/el1/base<br/>/data/storage/el2/base<br/>分布式文件保存在以下目录:<br/>/data/storage/el2/distributedfiles<br/>数据库文件保存在以下目录:<br/>/data/storage/el1/database<br/>/data/storage/el2/database |
## 开发示例
- 获取文件系统数据分区剩余空间大小。
```ts
import statvfs from '@ohos.file.statvfs';
let path = "/data";
statvfs.getFreeSize(path, (err, number) => {
if (err) {
console.error(`Invoke getFreeSize failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info(`Invoke getFreeSize succeeded, size is ${number}`);
}
});
```
- 获取当前应用的存储空间大小。
```ts
import storageStatistics from "@ohos.file.storageStatistics";
storageStatistics.getCurrentBundleStats((err, bundleStats) => {
if (err) {
console.error(`Invoke getCurrentBundleStats failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info(`Invoke getCurrentBundleStats succeeded, appsize is ${bundleStats.appSize}`);
}
});
```
# 应用沙箱目录
应用沙箱是一种以安全防护为目的的隔离机制,避免数据受到恶意路径穿越访问。在这种沙箱的保护机制下,应用可见的目录范围即为“应用沙箱目录”。
- 对于每个应用,系统会在内部存储空间映射出一个专属的“应用沙箱目录”,它是“[应用文件目录](app-file-overview.md)”与一部分系统文件(应用运行必需的少量系统文件)所在的目录组成的集合。
- 应用沙箱限制了应用可见的数据的最小范围。在“应用沙箱目录”中,应用仅能看到自己的应用文件以及少量的系统文件(应用运行必需的少量系统文件)。因此,本应用的文件也不为其他应用可见,从而保护了应用文件的安全。
- 应用可以在“[应用文件目录](app-file-overview.md)”下保存和处理自己的应用文件;系统文件及其目录对于应用是只读的;而应用若需访问[用户文件](user-file-overview.md),则需要通过特定API同时经过用户的相应授权才能进行。
下图展示了应用沙箱下,应用可访问的文件范围和方式。
**图1** 应用沙箱文件访问关系图  
![Application sandbox file access relationship](figures/application-sandbox-file-access-relationship.png)
## 应用沙箱目录与应用沙箱路径
在应用沙箱保护机制下,应用无法获知除自身应用文件目录之外的其他应用或用户的数据目录位置及存在。同时,所有应用的目录可见范围均经过权限隔离与文件路径挂载隔离,形成了独立的路径视图,屏蔽了实际物理路径:
- 如下图所示,在普通应用(也称三方应用)视角下,不仅可见的目录与文件数量限制到了最小范围,并且可见的目录与文件路径也与系统进程等其他进程看到的不同。我们将普通应用视角下看到的“应用沙箱目录”下某个文件或某个具体目录的路径,称为“应用沙箱路径”。
- 一般情况下,开发者的hdc shell环境等效于系统进程视角,因此“应用沙箱路径”与开发者使用hdc工具调试时看到的真实物理路径不同,其对应关系详见[应用沙箱路径和调试进程视角下的真实物理路径](send-file-to-app-sandbox.md#应用沙箱路径和调试进程视角下的真实物理路径)
- 从实际物理路径推导物理路径与沙箱路径并不是1:1的映射关系,沙箱路径总是少于系统进程视角可见的物理路径。有些调试进程视角下的物理路径在对应的应用沙箱目录是无法找到的,而沙箱路径总是能够找到其对应的物理路径。
**图2** 应用沙箱路径(不同权限与角色的进程下可见的文件路径不同)  
![Application sandbox path](figures/application-sandbox-path.png)
## 应用文件目录与应用文件路径
如前文所述,“应用沙箱目录”内分为两类:应用文件目录和系统文件目录。
系统文件目录对应用的可见范围由OpenHarmony系统预置,开发者无需关注。
在此主要介绍应用文件目录,如下图所示。应用文件目录下某个文件或某个具体目录的路径称为应用文件路径。应用文件目录下的各个文件路径,具备不同的属性和特征。
**图3** 应用文件目录结构图  
![Application file directory structure](figures/application-file-directory-structure.png)
1. 一级目录data/:代表应用文件目录。
2. 二级目录storage/:代表本应用持久化文件目录。
3. 三级目录el1/、el2/:代表不同文件加密类型。
- el1,设备级加密区:设备开机后即可访问的数据区。
- el2,用户级加密区:设备开机后,需要至少一次解锁对应用户的锁屏界面(密码、指纹、人脸等方式或无密码状态)后,才能够访问的加密数据区。<br>
应用如无特殊需要,应将数据存放在el2加密目录下,以尽可能保证数据安全。但是对于某些场景,一些应用文件需要在用户解锁前就可被访问,例如时钟、闹铃、壁纸等,此时应用需要将这些文件存放到设备级加密区(el1)。切换应用文件加密类型目录的方法请参见[获取和修改加密分区](../application-models/application-context-stage.md#获取和修改加密分区)
4. 四级、五级目录:
通过ApplicationContext可以获取base下的files、cache、preferences、temp、distributedfiles等目录的应用文件路径,应用全局信息可以存放在这些目录下。
通过UIAbilityContext、AbilityStageContext、ExtensionContext可以获取hap级别应用文件路径。HAP信息可以存放在这些目录下,存放在此目录的文件会跟随HAP的卸载而删除,不会影响app级别目录下的文件。在开发态,一个应用包含一个或者多个HAP,详见[Stage模型应用程序包结构](../quick-start/application-package-structure-stage.md)
Context上下文获取及上述应用文件路径的获取,详见[应用上下文Context](../application-models/application-context-stage.md)
> **说明:**
> - 禁止直接使用上图中四级目录之前的目录名组成的路径字符串,否则可能导致后续应用版本因应用文件路径变化导致不兼容问题。
>
> - 应通过Context属性获取应用文件路径,包括但不限于上图中绿色背景的路径。
应用文件路径具体说明及生命周期如下表所示。
**表1** 应用文件路径详细说明
| 目录名 | Context属性名称 | 类型 | 说明 |
| -------- | -------- | -------- | -------- |
| bundle | bundleCodeDir | 安装文件路径 | 应用安装后的app的hap资源包所在目录;随应用卸载而清理。 |
| base | NA | 本设备文件路径 | 应用在本设备上存放持久化数据的目录,子目录包含files/、cache/、temp/和haps/;随应用卸载而清理。 |
| database | databaseDir | 数据库路径 | 应用在el1加密条件下存放通过分布式数据库服务操作的文件目录;随应用卸载而清理。 |
| distributedfiles | distributedFilesDir | 分布式文件路径 | 应用在el2加密条件下存放分布式文件的目录,应用将文件放入该目录可分布式跨设备直接访问;随应用卸载而清理。 |
| files | filesDir | 应用通用文件路径 | 应用在本设备内部存储上通用的存放默认长期保存的文件路径;随应用卸载而清理。 |
| cache | cacheDir | 应用缓存文件路径 | 应用在本设备内部存储上用于缓存下载的文件或可重新生成的缓存文件的路径,应用cache目录大小超过配额或者系统空间达到一定条件,自动触发清理该目录下文件;用户通过系统空间管理类应用也可能触发清理该目录。应用需判断文件是否仍存在,决策是否需重新缓存该文件。 |
| preferences | preferencesDir | 应用首选项文件路径 | 应用在本设备内部存储上通过数据库API存储配置类或首选项的目录;随应用卸载而清理。详见[通过用户首选项实现数据持久化](../database/data-persistence-by-preferences.md)。 |
| temp | tempDir | 应用临时文件路径 | 应用在本设备内部存储上仅在应用运行期间产生和需要的文件,应用退出后即清理。 |
对于上述各类应用文件路径,常见使用场景如下:
- 安装文件路径
可以用于存储应用的代码资源数据,主要包括应用安装的HAP资源包、可重复使用的库文件以及插件资源等。此路径下存储的代码资源数据可以被用于动态加载。
- 数据库路径
仅用于保存应用的私有数据库数据,主要包括数据库文件等。此路径下仅适用于存储分布式数据库相关文件数据。
- 分布式文件路径
可以用于保存应用分布式场景下的数据,主要包括应用多设备共享文件、应用多设备备份文件、应用多设备群组协助文件。此路径下存储这些数据,使得应用更加适合多设备使用场景。
- 应用通用文件路径
可以用于保存应用的任何私有数据,主要包括用户持久性文件、图片、媒体文件以及日志文件等。此路径下存储这些数据,使得数据保持私有、安全且持久有效。
- 应用缓存文件路径
可以用于保存应用的缓存数据,主要包括离线数据、图片缓存、数据库备份以及临时文件等。此路径下存储的数据可能会被系统自动清理,因此不要存储重要数据。
- 应用首选项文件路径
可以用于保存应用的首选项数据,主要包括应用首选项文件以及配置文件等。此路径下仅适用于存储小量数据。
- 应用临时文件路径
可以用于保存应用的临时生成的数据,主要包括数据库缓存、图片缓存、临时日志文件、以及下载的应用安装包文件等。此路径下存储使用后即可删除的数据。
# 开发用户文件管理器(仅对系统应用开放)
OpenHarmony预置了FileManager文件管理器。系统应用开发者也可以根据需要,按以下指导自行开发文件管理器。
## 接口说明
开发用户文件管理器的相关API详细介绍请参见[API参考](../reference/apis/js-apis-fileAccess.md)
## 开发步骤
1. 权限配置和导入模块。
申请ohos.permission.FILE_ACCESS_MANAGER和ohos.permission.GET_BUNDLE_INFO_PRIVILEGED权限,配置方式请参见[访问控制授权申请](../security/accesstoken-guidelines.md)
> **说明:**
>
> ohos.permission.FILE_ACCESS_MANAGER是使用文件访问框架接口的基础权限。
>
> ohos.permission.GET_BUNDLE_INFO_PRIVILEGED权限可以用于查询系统内当前支持的文件管理服务端应用信息。
2. 导入依赖模块。
```ts
import fileAccess from '@ohos.file.fileAccess';
import fileExtensionInfo from '@ohos.file.fileExtensionInfo';
```
其中fileAccess提供了文件基础操作的API,fileExtensionInfo提供了应用开发的关键结构体。
3. 查询设备列表。
开发者可以获取当前系统所有文件管理服务端管理的设备属性,也可以获取某个文件管理服务端管理的设备属性。应用开发者可以按需过滤设备。
在文件访问框架中,使用RootInfo用于表示设备的属性信息。以下示例可以获取所有设备的RootInfo。
```ts
// 创建连接系统内所有文件管理服务端的helper对象
let fileAccessHelperAllServer = null;
createFileAccessHelper() {
try { // this.context是EntryAbility传过来的Context
fileAccessHelperAllServer = fileAccess.createFileAccessHelper(this.context);
if (!fileAccessHelperAllServer) {
console.error("createFileAccessHelper interface returns an undefined object");
}
} catch (error) {
console.error("createFileAccessHelper failed, errCode:" + error.code + ", errMessage:" + error.message);
}
}
async getRoots() {
let rootIterator = null;
let rootInfos = [];
let isDone = false;
try {
rootIterator = await fileAccessHelperAllServer.getRoots();
if (!rootIterator) {
console.error("getRoots interface returns an undefined object");
return;
}
while (!isDone) {
let result = rootIterator.next();
console.info("next result = " + JSON.stringify(result));
isDone = result.done;
if (!isDone)
rootinfos.push(result.value);
}
} catch (error) {
console.error("getRoots failed, errCode:" + error.code + ", errMessage:" + error.message);
}
}
```
4. 浏览目录。
在文件访问框架中,使用FileInfo表示一个文件(目录)的基础信息。开发者可以使用listfile接口遍历下一级所有文件(目录)的迭代器对象;也可以通过scanfile过滤指定目录,获取满足条件的迭代器对象。
listfile和scanfile接口当前支持RootInfo对象调用,可用于支撑遍历下一级文件或过滤整个目录树。同时,接口也支持FileInfo对象调用,用于支撑遍历下一级文件或过滤指定目录。
```ts
// 从根目录开始
let rootInfo = rootinfos[0];
let fileInfos = [];
let isDone = false;
let filter = {suffix : [".txt", ".jpg", ".xlsx"]}; // 设定过滤条件
try {
let fileIterator = rootInfo.listFile(); // 遍历设备rootinfos[0]的根目录,返回迭代器对象
// let fileIterator = rootInfo.scanFile(filter); // 过滤设备rootinfos[0]满足指定条件的文件信息,返回迭代对象
if (!fileIterator) {
console.error("listFile interface returns an undefined object");
return;
}
while (!isDone) {
let result = fileIterator.next();
console.info("next result = " + JSON.stringify(result));
isDone = result.done;
if (!isDone)
fileInfos.push(result.value);
}
} catch (error) {
console.error("listFile failed, errCode:" + error.code + ", errMessage:" + error.message);
}
// 从指定的目录开始
let fileInfoDir = fileInfos[0]; // fileInfoDir 表示某个目录信息
let subFileInfos = [];
let isDone = false;
let filter = {suffix : [".txt", ".jpg", ".xlsx"]}; // 设定过滤条件
try {
let fileIterator = fileInfoDir.listFile(); // 遍历特定的目录fileinfo,返回迭代器对象
// let fileIterator = rootInfo.scanFile(filter); // 过滤特定的目录fileinfo,返回迭代器对象
if (!fileIterator) {
console.error("listFile interface returns an undefined object");
return;
}
while (!isDone) {
let result = fileIterator.next();
console.info("next result = " + JSON.stringify(result));
isDone = result.done;
if (!isDone)
subfileInfos.push(result.value);
}
} catch (error) {
console.error("listFile failed, errCode:" + error.code + ", errMessage:" + error.message);
}
```
5. 操作文件或目录。
开发者可以集成文件访问框架的接口,完成一些用户行为,比如删除文件(目录)、重命名文件(目录)、新建文件(目录)、移动文件(目录)等。以下示例展示了如何创建一个文件,其他接口请参见[API参考](../reference/apis/js-apis-fileAccess.md)
```ts
// 以本地设备为例
// 创建文件
// 示例代码sourceUri是Download目录的fileinfo中的URI
// 开发者应根据自己实际获取fileinfo的URI进行开发
let sourceUri = "datashare:///media/file/6";
let displayName = "file1";
let fileUri = null;
try {
// fileAccessHelper 参考 fileAccess.createFileAccessHelper 示例代码获取
fileUri = await fileAccessHelper.createFile(sourceUri, displayName);
if (!fileUri) {
console.error("createFile return undefined object");
return;
}
console.info("createFile sucess, fileUri: " + JSON.stringify(fileUri));
} catch (error) {
console.error("createFile failed, errCode:" + error.code + ", errMessage:" + error.message);
};
```
# 分布式文件系统概述
分布式文件系统(hmdfs,Harmony Distributed File System)提供跨设备的文件访问能力,适用于如下场景:
- 两台设备组网,用户可以利用一台设备上的编辑软件编辑另外一台设备上的文档。
- 平板保存的音乐,车载系统直接可见并可播放。
- 户外拍摄的照片,回家打开平板直接访问原设备拍摄的照片。
hmdfs在分布式软总线动态组网的基础上,为网络上各个设备结点提供一个全局一致的访问视图,支持开发者通过基础文件系统接口进行读写访问,具有高性能、低延时等优点。
## 分布式文件系统架构
![Distributed File System Architecture](figures/distributed-file-system-architecture.png)
- distributedfile_daemon:主要负责设备上线监听、通过软总线建立链路,并根据分布式的设备安全等级执行不同的数据流转策略。
- hmdfs:实现在内核的网络文件系统,包括缓存管理、文件访问、元数据管理和冲突管理等。
- 缓存管理
- 设备分布式组网后,hmdfs提供文件的互访能力,但不会主动进行文件数据传输和拷贝。如果应用需要将数据保存到本地,需主动拷贝。
- hmdfs保证Close-to-Open的一致性,即一端写关闭后,另外一端可以读取到最新数据,不保证文件内容的实时一致性。
- 数据在远端写入,但是由于网络原因未及时回刷,文件系统会在下次网络接入时回刷本地,但是如果远端已修改则无法回刷。
- 文件访问
- 文件访问接口与本地一致([ohos.file.fs](../reference/apis/js-apis-file-fs.md))。
- 如果文件在本地,则堆叠访问本地文件系统。
- 如果文件在其他设备,则同步网络访问远端设备文件。
> **说明:**
>
> symlink:不支持。
- 元数据管理
- 分布式组网下,文件一端创建、删除、修改,另一端可以“立即”查看到最新文件,看到速度取决于网络情况。
- 远端设备离线后,该设备数据将不再在本端设备呈现。但由于设备离线的感知具有延迟,可能会造成部分消息4s超时,因此开发者需要考虑接口的网络超时或一些文件虽然可以看到,但实际设备可能已离线的场景。
- 冲突处理
- 本地与远端冲突 ,远端文件被重命名,看到的同名文件是本地同名文件,远端文件被重命名。
- 远端多个设备冲突,以接入本设备ID为顺序,显示设备ID小的同名文件,其他文件被依次重命名。
- 如果组网场景,目录树下已经有远端文件,创建同名文件,提示文件已存在。
- 冲突文件显示_conflict_dev后依次加id,id从1自动递增。
- 同名目录之间仅融合不存在冲突,文件和远端目录同名冲突,远端目录后缀加_remote_directory。
# 跨设备文件访问
分布式文件系统为应用提供了跨设备文件访问的能力,开发者在多个设备安装同一应用时,通过[基础文件接口](app-file-access-mgmt.md),可跨设备读写其他设备该应用分布式文件路径(/data/storage/el2/distributedfiles/)下的文件。例如:多设备数据流转的场景,设备组网互联之后,设备A上的应用可访问设备B同应用分布式路径下的文件,当期望应用文件被其他设备访问时,只需将文件移动到分布式文件路径即可。
## 开发步骤
1. 完成分布式组网。
首先将需要进行跨设备访问的设备连接到同一局域网中,同帐号认证完成组网。
2. 访问跨设备文件。
同一应用不同设备之间实现跨设备文件访问,只需要将对应的文件放在应用沙箱的分布式文件路径即可。
设备A上在分布式路径下创建测试文件,并写入内容。示例中的context的获取方式请参见[获取UIAbility的上下文信息](../application-models/uiability-usage.md#获取uiability的上下文信息)
```ts
import fs from '@ohos.file.fs';
let context = ...; // 获取设备A的UIAbilityContext信息
let pathDir = context.distributedFilesDir;
// 获取分布式目录的文件路径
let filePath = pathDir + '/test.txt';
try {
// 在分布式目录下创建文件
let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
console.info('Succeeded in createing.');
// 向文件中写入内容
fs.writeSync(file.fd, 'content');
// 关闭文件
fs.closeSync(file.fd);
} catch (err) {
console.error(`Failed to openSync / writeSync / closeSync. Code: ${err.code}, message: ${err.message}`);
}
```
设备B上在分布式路径下读取测试文件。
```ts
import fs from '@ohos.file.fs';
let context = ...; // 获取设备B的UIAbilityContext信息
let pathDir = context.distributedFilesDir;
// 获取分布式目录的文件路径
let filePath = pathDir + '/test.txt';
try {
// 打开分布式目录下的文件
let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
// 定义接收读取数据的缓存
let buffer = new ArrayBuffer(4096);
// 读取文件的内容,返回值是读取到的字节个数
let num = fs.readSync(file.fd, buffer, {
offset: 0
});
// 打印读取到的文件数据
console.info('read result: ' + String.fromCharCode.apply(null, new Uint8Array(buffer.slice(0, num))));
} catch (err) {
console.error(`Failed to openSync / readSync. Code: ${err.code}, message: ${err.message}`);
}
```
# 用户公共文件访问框架概述
在搭载OpenHarmony 3.2 (API 9)及更高版本的设备上,应用可以基于FileAccessFramework(简称为FAF)对本地公共文件、分布式设备文件、外部存储设备文件、多用户共享文件进行访问。
出于对用户数据的隐私安全考虑,目前此框架仅支持用户通过**文件管理器****文件选择器**对文件访问服务端进行操作,包括创建、打开、删除、重命名、移动等。
应用卸载,不会影响到用户数据,实际用户数据仍保留在对应设备中。
其它系统应用如需要访问本地公共文件,请参考使用[媒体库接口](medialibrary-filepath-guidelines.md)
> **注意:**
> 1:如果应用是系统非管理类应用,比如:系统图库,请优先考虑直接使用媒体库接口,媒体库接口提供了一系列直接操作文件的接口。
> 2:FAF 接口与媒体库接口原则上不能混用。
## FileAccessFramework机制介绍
FAF依托于OpenHarmony上[ExtensionAbility机制](../application-models/extensionability-overview.md),实现了一套对外提供能力的统一接口。应用可以通过这套接口预览和操作公共文件,实现自己的逻辑。
有兴趣的开发者,可以预览我们的[源码仓](https://gitee.com/openharmony/filemanagement_user_file_service),提出您宝贵的意见。
基于FAF进行文件操作的全流程,包含以下几个元素,如图所示:
**图1 公共文件操作层次图**
![](figures/公共文件操作层次图.png)
- **文件访问客户端应用** - 需要访问或操作公共文件的应用。通过拉起文件选择器,用户可以在可视化界面上进行文件操作。
- **文件选择器应用** - 可以让用户访问所有共享数据集的系统应用。通过使用 FAF 的对上接口,完成各种文件操作。
- **文件访问服务端应用** - 系统内支持将数据集进行共享的服务。目前有[UserFileManager](https://gitee.com/openharmony/multimedia_medialibrary_standard)、ExternalFileManager等。前者管理了本地磁盘、分布式设备的数据集,后者管理了SD卡、U盘等多种外置存储设备的数据集。开发者也可以基于 FAF 的服务端配置,共享自己的数据集。
FAF提供的主要功能:
- 可以让用户浏览系统内所有文件服务端应用提供的数据集,而不仅仅是单一应用的数据集。
- 客户端应用不需要获取FAF的使用权限,直接通过选择器应用操作文件。
- 支持访问多个临时挂载的设备,比如外置存储卡、分布式设备等。
## 数据模型
FAF 中数据模型主要通过URI、FileInfo、RootInfo 进行传递。详情参考[fileExtension](../reference/apis/js-apis-fileExtensionInfo.md)。文件访问服务端应用可以通过 FileAccessExtensionAbility API,将自身的数据安全的共享出去。
**图2 公共文件访问框架数据流**
![](figures/公共文件访问框架数据流.png)
注意事项:
- 在FAF中,文件访问客户端和文件访问服务端并不直接交互。只需要具备拉起文件选择器应用的权限即可。
- 文件选择器应用会为用户提供标准的文档访问界面,即使底层的文件访问服务端相互之间差异很大,一致性也不受影响。
\ No newline at end of file
# 文件管理概述
在操作系统中,存在各种各样的数据,按数据结构可分为:
- 结构化数据:能够用统一的数据模型加以描述的数据。常见的是各类数据库数据。在应用开发中,对结构化数据的开发活动隶属于[数据管理模块](database/data-mgmt-overview.md)
- 非结构化数据:指数据结构不规则或不完整,没有预定义的数据结构/模型,不方便用数据库二维逻辑表来表现的数据。常见的是各类文件,如文档、图片、音频、视频等。在应用开发中,对非结构化数据的开发活动隶属于文件管理模块,将在下文展开介绍。
在文件管理模块中,按文件所有者的不同,有如下文件分类模型,其示意图如下面文件分类模型示意图:
- [应用文件](app-file-overview.md):文件所有者为应用,包括应用安装文件、应用资源文件、应用缓存文件等。
- [用户文件](user-file-overview.md):文件所有者为登录到该终端设备的用户,包括用户私有的图片、视频、音频、文档等。
- 系统文件:与应用和用户无关的其他文件,包括公共库、设备文件、系统资源文件等。这类文件不需要开发者进行文件管理,本文不展开介绍。
按文件系统管理的文件存储位置(数据源位置)的不同,有如下文件系统分类模型:
- 本地文件系统:提供本地设备或外置存储设备(如U盘、移动硬盘)的文件访问能力。本地文件系统是最基本的文件系统,本文不展开介绍。
- [分布式文件系统](distributed-fs-overview.md):提供跨设备的文件访问能力。所谓跨设备,指文件不一定存储在本地设备或外置存储设备,而是通过计算机网络与其他分布式设备相连。
**图1** 文件分类模型示意图
![File classification model](figures/file-classification-model.png)
# 文件选择器使用指导
文件选择器(FilePicker)是OpenHarmony中预置的系统应用,为用户提供文件选择及保存功能。具体实现可以参考[FilePicker代码仓库](https://gitee.com/openharmony/applications_filepicker)
应用可以通过FilePicker的两种模式实现文件选择和文件保存的功能。
- choose模式(文件选择):当应用需要选择并上传、发送设备中的文件(包括图片、音视频等媒体资源)时,可以选择该模式。拉起FilePicker的choose模式窗口,系统将弹出弹框供用户选择具体文件。用户通过界面选择目标文件并点击“上传”按钮,应用将接收到FilePicker传回的目标文件uri。
- save模式(文件保存):当应用需要下载保存文件(包括图片、音视频等媒体资源)时,可以选择该模式。拉起FilePicker的save模式窗口,系统将弹出弹框供用户选择保存文件的目标路径。用户通过界面选择目标路径并点击“保存”按钮,应用将接收到FilePicker传回的已保存文件uri。
## 开发指导
> **说明:**
> FilePicker仅支持基于Stage模型开发的应用拉起。<br/>
> Stage模型介绍请参考[应用模型解读](../application-models/application-model-description.md)。
使用不同的参数调用[AbilityContext.startAbilityForResult(want, options)](../reference/apis/js-apis-inner-application-uiAbilityContext.md#uiabilitycontextstartabilityforresult-1)可以拉起FilePicker不同模式的窗口。
开发者需要通过[Want](../reference/apis/js-apis-application-want.md)指定"bundleName"和"abilityName"来拉起FilePicker,具体写法可参见下方的示例代码。
同时,开发者还需要设置属性Want.parameters,来指定FilePicker拉起的模式以及文件保存的名称:
- 文件选择:仅需要设置FilePicker拉起的窗口模式为`'startMode': 'choose'`
- 文件保存:除了设置FilePicker拉起的窗口模式为`'startMode': 'save'`,还需要设置文件保存名称`'saveFile'`
可以通过设置类型为[StartOptions](../reference/apis/js-apis-app-ability-startOptions.md)的入参options来指定弹出窗口样式,推荐设置为`windowMode: 102`,即自由悬浮形式窗口。
> **注意:**
> 1. save模式下,用户保存路径根据保存文件名执行强校验,用户保存路径参考[公共目录路径支持的文件格式](medialibrary-filepath-guidelines.md)。
> 2. 用户选择目标文件保存路径后,如存在同名文件,FilePicker将弹窗提醒用户是否覆盖原文件。
ArkTS语言示例代码如下:
```ts
// 拉起FilePicker选择文件
globalThis.context.startAbilityForResult(
{
action: "ohos.want.action.OPEN_FILE",
parameters: {
'startMode': 'choose', //choose or save
}
},
{ windowMode: 102 }
)
// 拉起FilePicker保存文件
globalThis.context.startAbilityForResult(
{
action: "ohos.want.action.CREATE_FILE",
parameters: {
'startMode': 'save', //choose or save
'saveFile': 'test.jpg',
}
},
{ windowMode: 102 }
)
// FilePicker返回给startAbilityForResult的数据
let abilityResult = {
resultCode: resultCode,
want: {
parameters: {
'startMode': startMode,
'result': result
}
}
}
globalThis.context.terminateSelfWithResult(abilityResult)
```
# 管理外置存储设备(仅对系统应用开放)
外置存储设备具备可插拔属性,因此系统提供了设备插拔事件的监听及挂载功能,用于管理外置存储设备。
外置存储设备的管理由StorageManager和StorageDaemon两个服务完成。StorageDaemon实现底层的的监听挂载等功能;StorageManager则对系统应用提供状态变更通知、查询和管理能力。
**图1** 外置存储设备管理示意图  
![External storage device management](figures/external-storage-device-management.png)
- 插入外卡时,StorageDaemon进程通过netlink监听获取到外卡插入事件,创建对应的磁盘设备以及卷设备,此时,已创建的卷设备状态为卸载状态(UNMOUNTED)。
- StorageDaemon进程在创建完卷设备后,会对卷设备进行检查,此时卷状态为检查状态(CHECKING)。
- 检查成功后,会对卷设备进行挂载,挂载成功后,卷状态更改为挂载状态(MOUNTED),并通知StorageManager发送COMMON_EVENT_VOLUME_MOUNTED广播。
- 检查失败,则返回卸载状态(UNMOUNTED)。
- 当卷设备处于挂载状态时:
- 拔出卷设备,会直接删除相关卷设备信息,并发送COMMON_EVENT_VOLUME_BAD_REMOVAL广播。
- 当用户选择弹出时,卷状态设备更改为正在弹出状态(EJECTING),并发送COMMON_EVENT_VOLUME_EJECT广播。StorageDaemon进程将卷设备卸载成功后,卷状态更改为卸载状态(UNMOUNTED),并发送COMMON_EVENT_VOLUME_UNMOUNTED广播。
- 当卷设备处于卸载状态时,拔出卷设备会删除相关卷设备信息,并发送COMMON_EVENT_VOLUME_REMOVED广播。
## 接口说明
外置存储设备管理相关API的详细介绍请参见[API参考](../reference/apis/js-apis-file-volumemanager.md)
各类广播传递的相关参数,请见下表。
**表1** 广播传递的参数
| 广播类型 | 参数 |
| -------- | -------- |
| usual.event.data.VOLUME_REMOVED | id:卷设备ID<br/>diskId:卷设备所属磁盘设备ID |
| usual.event.data.VOLUME_UNMOUNTED | id:卷设备ID<br/>diskId:卷设备所属磁盘设备ID<br/>volumeState:卷设备状态 |
| usual.event.data.VOLUME_MOUNTED | id:卷设备ID<br/>diskId:卷设备所属磁盘设备ID<br/>volumeState:卷设备状态<br/>fsUuid:卷设备uuid<br/>path:卷设备挂载路径 |
| usual.event.data.VOLUME_BAD_REMOVAL | id:卷设备ID<br/>diskId:卷设备所属磁盘设备ID |
| usual.event.data.VOLUME_EJECT | id:卷设备ID<br/>diskId:卷设备所属磁盘设备ID<br/>volumeState:卷设备状态 |
## 开发步骤
开发者通过订阅卷设备相关的广播事件来感知外置存储的插入,通过广播传递的信息获取卷设备信息后可以对卷设备进行查询以及管理操作。
1. 获取权限。
订阅卷设备广播事件需要申请ohos.permission.STORAGE_MANAGER权限,配置方式请参见[配置文件权限声明](../security/accesstoken-guidelines.md#配置文件权限声明)
2. 订阅广播事件。
需订阅的事件如下:
- 卷设备移除:"usual.event.data.VOLUME_REMOVED"
- 卷设备卸载:"usual.event.data.VOLUME_UNMOUNTED"
- 卷设备挂载:"usual.event.data.VOLUME_MOUNTED"
- 卷设备异常移除:"usual.event.data.VOLUME_BAD_REMOVAL"
- 卷设备正在弹出:"usual.event.data.VOLUME_EJECT"
```ts
import CommonEvent from '@ohos.commonEventManager';
import volumeManager from '@ohos.file.volumeManager';
const subscribeInfo = {
events: [
"usual.event.data.VOLUME_REMOVED",
"usual.event.data.VOLUME_UNMOUNTED",
"usual.event.data.VOLUME_MOUNTED",
"usual.event.data.VOLUME_BAD_REMOVAL",
"usual.event.data.VOLUME_EJECT"
]
};
let subscriber = await CommonEvent.createSubscriber(subscribeInfo);
```
3. 收到广播通知后获取卷设备信息。
```ts
CommonEvent.subscribe(subscriber, function (err, data) {
if (data.event === 'usual.event.data.VOLUME_MOUNTED') {
// 开发者可以通过广播传递的卷设备信息来管理卷设备
let volId = data.parameters.id;
volumeManager.getVolumeById(volId, function(error, vol) {
if (error) {
console.error('volumeManager getVolumeById failed');
} else {
console.info('volumeManager getVolumeById successfully, the volume state is ' + vol.state);
}
})
}
})
```
# 相册资源使用指导
mediaLibrary提供相册相关的接口,供开发者创建、删除相册,获取相册中的图片资源等。
> **说明:**
>
> 在进行功能开发前,请开发者查阅[媒体库开发概述](medialibrary-overview.md),了解如何获取媒体库实例和如何申请媒体库功能开发相关权限。
为了保证应用的运行效率,大部分MediaLibrary调用都是异步的,对于异步调用的API均提供了callback和Promise两种方式,以下示例均采用Promise函数,更多方式可以查阅[API参考](../reference/apis/js-apis-medialibrary.md)
## 获取相册中的图片/视频
获取相册中的图片、视频有两种方式:
一是通过[MediaLibrary.getFileAssets](../reference/apis/js-apis-medialibrary.md#getfileassets7-1)指定相册以获取媒体资源,参考[获取指定相册的媒体资源](medialibrary-resource-guidelines.md#指定相册)
二是通过[Album.getFileAssets](../reference/apis/js-apis-medialibrary.md#getfileassets7-3)使用相册Album实例获取媒体资源,参考[获取相册中的图片或视频](medialibrary-resource-guidelines.md#获取相册中的图片或视频)
## 创建相册
通过[MediaLibrary.createAsset](../reference/apis/js-apis-medialibrary.md#createasset8-1)可以创建媒体资源,可以通过创建图片或视频文件时设置的相对路径,创建出相册。相对路径的命名即为相册名称。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读写权限“ohos.permission.WRITE_MEDIA”。
下面以创建相册myAlbum为例。
**开发步骤**
1. 调用getPublicDirectory获取文件公共路径。
获取文件公共路径的更多指导可参考[获取文件保存的公共目录](medialibrary-filepath-guidelines.md#获取文件保存的公共目录)
2. 调用createAsset新建图片,并设置相对路径为path+'myAlbum/'。
即在创建相册的同时,往里面放了一张图片。
```ts
async function example() {
let mediaType = mediaLibrary.MediaType.IMAGE;
let DIR_IMAGE = mediaLibrary.DirectoryType.DIR_IMAGE;
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
const path = await media.getPublicDirectory(DIR_IMAGE);
//myAlbum为新建文件保存路径,也是新建相册的名称
media.createAsset(mediaType, 'test.jpg', path + 'myAlbum/', (err, fileAsset) => {
if (fileAsset === undefined) {
console.error('createAlbum failed, message = ' + err);
} else {
console.info('createAlbum successfully, message = ' + JSON.stringify(fileAsset));
}
});
}
```
## 重命名相册
重命名修改的是相册的FileAsset.albumName属性,即相册名称。修改后再通过[Album.commitModify](../reference/apis/js-apis-medialibrary.md#commitmodify8-3)更新到数据库中。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读写权限“ohos.permission.WRITE_MEDIA”。
下面以重命名相册为“newAlbum“为例。
**开发步骤**
1. 建立检索条件,用于获取目标相册。
2. 调用getAlbums获取相册列表。
3. 将相册重命名为“newAlbum“。
4. 调用Album.commitModify将更新的相册属性修改到数据库中。
```ts
async function example() {
let AlbumNoArgsfetchOp = {
selections: '',
selectionArgs: [],
};
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
let albumList = await media.getAlbums(AlbumNoArgsfetchOp);
let album = albumList[0];
album.albumName = 'newAlbum';
//回调返回空
album.commitModify().then(() => {
console.info("albumRename successfully");
}).catch((err) => {
console.error("albumRename failed with error: " + err);
});
}
```
# 文件路径使用指导
Openharmony上用户数据统一由媒体库进行管理,用户数据用户数据可以通过mediaLibrary提供的接口能力进行访问和操作。
> **说明:**
>
> 在进行功能开发前,请开发者查阅[媒体库开发概述](medialibrary-overview.md),了解如何获取媒体库实例和如何申请媒体库功能开发相关权限。
为了保证应用的运行效率,大部分MediaLibrary调用都是异步的,对于异步调用的API均提供了callback和Promise两种方式,以下示例均采用Promise函数,更多方式可以查阅[API参考](../reference/apis/js-apis-medialibrary.md)
## 公共目录路径支持的文件格式
在使用文件路径进行开发之前,需要了解各公共目录路径支持的文件格式说明。
> **注意:** <br/>
> 下表仅表示系统能识别的文件类型,在具体的开发中,开发者需要关注对应接口支持的文件格式。<br/> 如image编码功能只支持.jpeg和.webp,解码功能只支持.jpg .png .gif .bmp .webp RAW。
| 目录路径 | 目录类型 | 媒体类型 | 说明 | 支持的文件格式 |
| ---------- | ------------- | ------------- | -------------- | ------------------------------------------------------------ |
| Camera/ | DIR_CAMERA | VIDEO amd IMAGE | 相机拍摄图片与录像的存放路径,目录与子目录下可以存放视频,图片类型文件。 | .bmp / .bm / .gif / .jpg /. jpeg / .jpe / .png / .webp / .raw / .svg / .heif / .mp4 / .3gp / .mpg / .mov / .webm / .mkv |
| Videos/ | DIR_VIDEO | VIDEO | 视频专有目录,目录与子目录下只可以存放视频类型文件。| .mp4 / .3gp / .mpg / .mov / .webm / .mkv |
| Pictures/ | DIR_IMAGE | IMAGE | 图片专有目录,目录与子目录下只可以存放图片类型文件。 | .bmp / .bm / .gif / .jpg /. jpeg / .jpe / .png / .webp / .raw / .svg / .heif |
| Audios/ | DIR_AUDIO | AUDIO |音频专有目录,目录与子目录下只可以存放音频类型文件。| .aac/.mp3/.flac/.wav/.ogg |
| Documents/ | DIR_DOCUMENTS | FILE |文档类型目录,目录与子目录下只可以存放音频,图片,视频以外类型文件。| - |
| Download/ | DIR_DOWNLOAD | ALLTYPE |下载文件存放目录,目录与子目录下文件类型不受限制。| - |
## 获取文件保存的公共目录
不同类型的文件会保存到不同的公共目录下,可以通过接口[getPublicDirectory](../reference/apis/js-apis-medialibrary.md#getpublicdirectory8-1)来获取公共目录路径。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读权限"ohos.permission.READ_MEDIA"。
下面以获取Camera文件保存的公共目录为例。
```ts
async function example(){
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
let DIR_CAMERA = mediaLibrary.DirectoryType.DIR_CAMERA;
const dicResult = await media.getPublicDirectory(DIR_CAMERA);
if (dicResult == 'Camera/') {
console.info('mediaLibraryTest : getPublicDirectory passed');
} else {
console.error('mediaLibraryTest : getPublicDirectory failed');
}
}
```
## 沙箱与公共路径间文件的复制
OpenHarmony提供应用沙箱机制,增加目录可见性数据访问防线,减少了应用数据和用户隐私信息泄露,建立了更加严格安全的应用沙盒隔离能力。
放在公共路径下的文件,用户可以通过系统应用“文件管理”、“图库”访问,但应用沙箱内的文件,只有应用本身可以访问。
### 复制文件
通过接口[mediaLibrary.FileAsset.open](../reference/apis/js-apis-medialibrary.md#open8-1)可以打开公共路径文件。
通过接口[fs.open](../reference/apis/js-apis-file-fs.md#fsopen)可以打开沙箱路径文件,沙箱路径必须通过应用上下文context进行访问。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读写权限"ohos.permission.READ_MEDIA, ohos.permission.WRITE_MEDIA"。
- 除了@ohos.multimedia.mediaLibrary外,还需要导入模块[@ohos.file.fs](../reference/apis/js-apis-file-fs.md)
- 测试文件 "testFile.txt" 已创建且有文件内容。
**开发步骤**
1. 调用[context.filesDir](../reference/apis/js-apis-file-fs.md)获取应用沙箱路径。
2. 调用MediaLibrary.getFileAssets和FetchFileResult.getFirstObject获取公共目录中的FileAsset实例。
3. 调用fs.open打开沙箱路径文件。
4. 调用fileAsset.open打开公共路径文件。
5. 调用[fs.copyfile](../reference/apis/js-apis-file-fs.md#fscopyfile)复制文件。
6. 调用fileAsset.close和[fs.close](../reference/apis/js-apis-file-fs.md#fsclose)关闭文件。
**示例1 将公共路径文件复制到沙箱路径下**
```ts
async function copyPublic2Sandbox() {
try {
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
let sandboxDirPath = context.filesDir;
let fileKeyObj = mediaLibrary.FileKey;
let fileAssetFetchOp = {
selections: fileKeyObj.DISPLAY_NAME + '= ?',
selectionArgs: ['testFile.txt'],
};
let fetchResult = await media.getFileAssets(fileAssetFetchOp);
let fileAsset = await fetchResult.getFirstObject();
let fdPub = await fileAsset.open('rw');
let fdSand = await fs.open(sandboxDirPath + '/testFile.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
await fs.copyFile(fdPub, fdSand.fd);
await fileAsset.close(fdPub);
await fs.close(fdSand.fd);
let content_sand = await fs.readText(sandboxDirPath + '/testFile.txt');
console.info('content read from sandbox file: ', content_sand)
} catch (err) {
console.info('[demo] copyPublic2Sandbox fail, err: ', err);
}
}
```
**示例2 将应用沙箱路径文件复制到公共路径**
```ts
async function copySandbox2Public() {
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
let sandboxDirPath = context.filesDir;
let DIR_DOCUMENTS = mediaLibrary.DirectoryType.DIR_DOCUMENTS;
const publicDirPath = await media.getPublicDirectory(DIR_DOCUMENTS);
try {
let fileAsset = await media.createAsset(mediaLibrary.MediaType.FILE, 'testFile02.txt', publicDirPath);
console.info('createFile successfully, message = ' + fileAsset);
} catch (err) {
console.error('createFile failed, message = ' + err);
}
try {
let fileKeyObj = mediaLibrary.FileKey;
let fileAssetFetchOp = {
selections: fileKeyObj.DISPLAY_NAME + '= ?',
selectionArgs: ['testFile02.txt'],
};
let fetchResult = await media.getFileAssets(fileAssetFetchOp);
var fileAsset = await fetchResult.getFirstObject();
} catch (err) {
console.error('file asset get failed, message = ' + err);
}
let fdPub = await fileAsset.open('rw');
let fdSand = await fs.open(sandboxDirPath + 'testFile.txt', fs.OpenMode.READ_WRITE);
await fs.copyFile(fdSand.fd, fdPub);
await fileAsset.close(fdPub);
await fs.close(fdSand.fd);
let fdPubRead = await fileAsset.open('rw');
try {
let arrayBuffer = new ArrayBuffer(4096);
await fs.read(fdPubRead, arrayBuffer);
var content_pub = String.fromCharCode(...new Uint8Array(arrayBuffer));
fileAsset.close(fdPubRead);
} catch (err) {
console.error('read text failed, message = ', err);
}
console.info('content read from public file: ', content_pub);
}
```
### 读写文件内容
通过[mediaLibrary](../reference/apis/js-apis-medialibrary.md)的接口FileAsset.open和FileAsset.close可以打开和关闭文件。通过[file.fs](../reference/apis/js-apis-file-fs.md)中的接口fs.read和fs.write可以读写文件。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读写权限"ohos.permission.READ_MEDIA, ohos.permission.WRITE_MEDIA"。
- 除了@ohos.multimedia.mediaLibrary外,还需要导入模块[@ohos.file.fs](../reference/apis/js-apis-file-fs.md)
**开发步骤**
1. 创建用于读写示例的文件。
```ts
async function example() {
let mediaType = mediaLibrary.MediaType.FILE;
let DIR_DOCUMENTS = mediaLibrary.DirectoryType.DIR_DOCUMENTS;
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
const path = await media.getPublicDirectory(DIR_DOCUMENTS);
media.createAsset(mediaType, "testFile.txt", path).then((asset) => {
console.info("createAsset successfully:" + JSON.stringify(asset));
}).catch((err) => {
console.error("createAsset failed with error: " + err);
});
}
```
2. 使用open打开文件。
3. 使用[fs.write](../reference/apis/js-apis-file-fs.md#fswrite)写入文件,以string形式传入写入数据。
4. 使用[fs.read](../reference/apis/js-apis-file-fs.md#fsread)读取文件,以 ArrayBuffer 形式保存读取结果。
5. 将ArrayBuffer转化为string,以string形式得到文件内容。
6. 使用close关闭文件。
**示例1 打开现有文件、向文件中写入**
```ts
async function writeOnlyPromise() {
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
let fileKeyObj = mediaLibrary.FileKey;
let fileAssetFetchOp = {
selections: fileKeyObj.DISPLAY_NAME + '= ?',
selectionArgs: ['testFile.txt'],
};
let fetchResult = await media.getFileAssets(fileAssetFetchOp);
let fileAsset = await fetchResult.getFirstObject();
console.info('fileAssetName: ', fileAsset.displayName);
try {
let fd = await fileAsset.open('w');
console.info('file descriptor: ', fd);
await fs.write(fd, "Write file test content.");
await fileAsset.close(fd);
} catch (err) {
console.error('write file failed, message = ', err);
}
}
```
**示例2 打开现有文件,读取文件内容**
```ts
async function readOnlyPromise() {
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
let fileKeyObj = mediaLibrary.FileKey;
let fileAssetFetchOp = {
selections: fileKeyObj.DISPLAY_NAME + '= ?' ,
selectionArgs: ['testFile.txt'],
};
let fetchResult = await media.getFileAssets(fileAssetFetchOp);
let fileAsset = await fetchResult.getFirstObject();
console.info('fileAssetName: ', fileAsset.displayName);
try {
let fd = await fileAsset.open('r');
let arrayBuffer = new ArrayBuffer(4096);
await fs.read(fd, arrayBuffer);
let fileContent = String.fromCharCode(...new Uint8Array(arrayBuffer));
globalThis.fileContent = fileContent;
globalThis.fileName = fileAsset.displayName;
console.info('file content: ', fileContent);
await fileAsset.close(fd);
} catch (err) {
console.error('read file failed, message = ', err);
}
}
```
# 媒体库开发概述
MediaLibrary提供媒体库相关能力,帮助开发者更方便地访问和修改媒体文件,具体分为:
- [媒体资源(音频、视频、图片文件等)相关](medialibrary-resource-guidelines.md),包括:
- 查询指定媒体资源
- 获取图片/视频
- 获取图片/视频缩略图
- 创建媒体资源
- 重命名媒体资源
- 将媒体资源放入回收站
- [文件路径相关](medialibrary-filepath-guidelines.md),包括:
- 获取文件保存的公共目录
- 沙箱与公共路径间文件的复制
- 读写文件内容
- [相册相关](medialibrary-album-guidelines.md),包括:
- 获取相册中的图片/视频
- 创建相册
- 重命名相册
> **说明:**<br/>
> 本开发指导基于API Version 9,仅适用于Stage模型。
应用需要先获取媒体库实例,才能访问和修改用户等个人媒体数据信息。媒体库涉及用户个人数据信息,所以应用需要向用户申请媒体库读写操作权限才能保证功能的正常运行。在使用媒体库相关接口时如无其他注明则默认在工程代码的pages/index.ets或者其他自创的ets文件中使用
开发者在使用MediaLibrary进行功能开发前,请先掌握以下内容:
- [获取媒体库实例](#获取媒体库实例)
- [申请媒体库功能相关权限](#申请媒体库功能相关权限)
## 获取媒体库实例
应用需要使用应用上下文Context通过接口[getMediaLibrary](../reference/apis/js-apis-medialibrary.md#medialibrarygetmedialibrary8),获取媒体库实例,用于访问和修改用户等个人媒体数据信息(如音频、视频、图片、文档等)。
**开发步骤**
1. 导入mediaLibrary模块以使用媒体库相关接口。
2. 通过getContext获取应用上下文。
3. 获取媒体库实例。
```ts
import mediaLibrary from '@ohos.multimedia.mediaLibrary';
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
```
## 申请媒体库功能相关权限
媒体库的读写操作需要相应权限,在申请权限前,请保证符合[权限使用的基本原则](../security/accesstoken-overview.md#权限使用的基本原则)。涉及的权限如下表。
| 权限名 | 说明 | 授权方式 |
| ------------------------------ | ------------------------------------------ | ---------- |
| ohos.permission.READ_MEDIA | 允许应用读取用户外部存储中的媒体文件信息。 | user_grant |
| ohos.permission.WRITE_MEDIA | 允许应用读写用户外部存储中的媒体文件信息。 | user_grant |
| ohos.permission.MEDIA_LOCATION | 允许应用访问用户媒体文件中的地理位置信息。 | user_grant |
以上权限的授权方式均为user_grant(用户授权),即开发者在module.json5文件中配置对应的权限后,需要使用接口[abilityAccessCtrl.requestPermissionsFromUser](../reference/apis/js-apis-abilityAccessCtrl.md#requestpermissionsfromuser9)去校验当前用户是否已授权。如果是,应用可以直接访问/操作目标对象;否则需要弹框向用户申请授权。
> **说明:**<br/>即使用户曾经授予权限,应用在调用受此权限保护的接口前,也应该先检查是否有权限。不能把之前授予的状态持久化,因为用户在动态授予后还可以通过“设置”取消应用的权限。
**开发步骤**
1. 在配置文件module.json5中声明权限。在配置文件的“module”标签内,增加“requestPermissions”标签,标签内容请根据实际情况填写。标签说明可参考[访问控制(权限)开发指导](../security/accesstoken-guidelines.md)
```json
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.MEDIA_LOCATION",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.READ_MEDIA",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.WRITE_MEDIA",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
}
]
}
}
```
2. 在Ability.ts中onWindowStageCreate里调用requestPermissionsFromUser进行权限校验,可以选择需要动态申请获取的权限自行添加相应代码
```ts
import UIAbility from '@ohos.app.ability.UIAbility';
import abilityAccessCtrl, {Permissions} from '@ohos.abilityAccessCtrl';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage) {
let list : Array<Permissions> = ['ohos.permission.READ_MEDIA', 'ohos.permission.WRITE_MEDIA'];
let permissionRequestResult;
let atManager = abilityAccessCtrl.createAtManager();
atManager.requestPermissionsFromUser(this.context, list, (err, result) => {
if (err) {
console.error('requestPermissionsFromUserError: ' + JSON.stringify(err));
} else {
permissionRequestResult = result;
console.info('permissionRequestResult: ' + JSON.stringify(permissionRequestResult));
}
});
}
}
```
# 媒体资源使用指导
应用可以通过mediaLibrary的接口,进行媒体资源(音频、视频、图片文件等)相关操作。
> **说明:**
>
> 在进行功能开发前,请开发者查阅[媒体库开发概述](medialibrary-overview.md),了解如何获取媒体库实例和如何申请媒体库功能开发相关权限。
为了保证应用的运行效率,大部分MediaLibrary调用都是异步的,对于异步调用的API均提供了callback和Promise两种方式,以下示例均采用Promise函数,更多方式可以查阅[API参考](../reference/apis/js-apis-medialibrary.md)
## 获取媒体资源
开发者可以根据特定的条件查询媒体资源,如指定类型、指定日期、指定相册等。
应用通过调用[MediaLibrary.getFileAssets](../reference/apis/js-apis-medialibrary.md#getfileassets7-1)获取媒体资源,并传入MediaFetchOptions对象指定检索条件。MediaFetchOptions.selections为检索条件,使用FileKey中的枚举值作为检索条件的列名;MediaFetchOptions.selectionArgs对应selections中检索条件列的值;除此之外,可以使用order(结果排序方式)、uri(文件URI)、networkId(注册设备网络ID)作为检索条件。
如果只想获取某个位置的对象(如第一个、最后一个、指定索引等),可以通过[FetchFileResult](../reference/apis/js-apis-medialibrary.md#fetchfileresult7)中的接口获取对应位置的媒体资源对象。本小节均采用getNextObject循环获取检索结果中的所有媒体资源。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读权限“ohos.permission.READ_MEDIA”。
### 指定媒体类型
下面以查询图片类型的媒体资源为例。
**开发步骤**
selections: FileKey.MEDIA_TYPE,根据媒体类型检索。
selectionArgs: MediaType.IMAGE,媒体类型为图片。
```ts
async function example() {
let fileKeyObj = mediaLibrary.FileKey;
let fileType = mediaLibrary.MediaType.IMAGE;
let option = {
selections: fileKeyObj.MEDIA_TYPE + '= ?',
selectionArgs: [fileType.toString()],
};
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
const fetchFileResult = await media.getFileAssets(option);
fetchFileResult.getFirstObject().then(async (fileAsset) => {
console.log('getFirstObject.displayName : ' + fileAsset.displayName);
for (let i = 1; i < fetchFileResult.getCount(); i++) {
let fileAsset = await fetchFileResult.getNextObject();
console.info('fileAsset.displayName ' + i + ': ' + fileAsset.displayName);
}
}).catch((err) => {
console.error('Failed to get first object: ' + err);
});
}
```
### 指定日期
下面以查询指定添加日期至今的所有媒体资源为例。实际开发中可以设置添加日期、修改日期、拍摄日期。
selections: FileKey.DATE_ADDED,根据文件添加日期检索。
selectionArgs:2022-8-5,具体添加时间的字符串。
```ts
async function example() {
let fileKeyObj = mediaLibrary.FileKey;
let option = {
selections: fileKeyObj.DATE_ADDED + '> ?',
selectionArgs: ['2022-8-5'],
};
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
const fetchFileResult = await media.getFileAssets(option);
fetchFileResult.getFirstObject().then(async (fileAsset) => {
console.info('getFirstObject.displayName : ' + fileAsset.displayName);
for (let i = 1; i < fetchFileResult.getCount(); i++) {
let fileAsset = await fetchFileResult.getNextObject();
console.info('fileAsset.displayName ' + i + ': ' + fileAsset.displayName);
}
}).catch((err) => {
console.error('Failed to get first object: ' + err);
});
}
```
### 按指定顺序排列
下面以查询图片并按文件添加日期降序排列为例。实际开发中可以设置升序(ASC)和降序(DESC)。
order: FileKey.DATE_ADDED,根据文件添加日期排序;并设置排列顺序为DESC降序。
```ts
async function example() {
let fileKeyObj = mediaLibrary.FileKey;
let fileType = mediaLibrary.MediaType.IMAGE;
let option = {
selections: fileKeyObj.MEDIA_TYPE + '= ?',
selectionArgs: [fileType.toString()],
order: fileKeyObj.DATE_ADDED + " DESC",
};
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
const fetchFileResult = await media.getFileAssets(option);
fetchFileResult.getFirstObject().then(async (fileAsset) => {
console.info('getFirstObject.displayName : ' + fileAsset.displayName);
for (let i = 1; i < fetchFileResult.getCount(); i++) {
let fileAsset = await fetchFileResult.getNextObject();
console.info('fileAsset.displayName ' + i + ': ' + fileAsset.displayName);
}
}).catch((err) => {
console.error('Failed to get first object: ' + err);
});
}
```
### 指定相册
下面以指定相册myAlbum为例。
selections: FileKey.ALBUM_NAME,根据相册名称检索。
selectionArgs:'myAlbum',具体相册名称。
```ts
async function example() {
let fileKeyObj = mediaLibrary.FileKey;
let option = {
selections: fileKeyObj.ALBUM_NAME + '= ?',
selectionArgs: ['myAlbum'],
};
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
const fetchFileResult = await media.getFileAssets(option);
if (albumList.length > 0) {
fetchFileResult.getFirstObject().then((album) => {
console.info('getFirstObject.displayName : ' + album.albumName);
}).catch((err) => {
console.error('Failed to get first object: ' + err);
});
} else {
console.info('getAlbum list is: 0');
}
}
```
## 获取相册中的图片或视频
获取相册的媒体资源有两种方式,一是通过[MediaLibrary.getFileAssets](../reference/apis/js-apis-medialibrary.md#getfileassets7-1)指定相册以获取媒体资源,参考[获取指定相册的媒体资源](#指定相册);二是通过[Album.getFileAssets](../reference/apis/js-apis-medialibrary.md#getfileassets7-3)使用相册Album实例获取媒体资源。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读权限“ohos.permission.READ_MEDIA”。
**开发步骤**
下面以获取相册名称为“新建相册1”的视频为例。
1. 建立检索条件,用于获取目的相册实例。
```ts
let fileKeyObj = mediaLibrary.FileKey;
let AlbumNoArgsFetchOp = {
selections: fileKeyObj.ALBUM_NAME + '= ?',
selectionArgs: ['新建相册1']
}
```
2. 建立检索条件,用于获取目的相册下的视频资源。
```ts
let fileKeyObj = mediaLibrary.FileKey;
let videoType = mediaLibrary.MediaType.VIDEO;
let videoFetchOp = {
selections: fileKeyObj.MEDIA_TYPE + '= ?',
selectionArgs: [videoType.toString()],
}
```
3. 通过Album.getFileAssets获取对应的资源。
以下为**完整示例**
```ts
async function getCameraImagePromise() {
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
let fileKeyObj = mediaLibrary.FileKey;
let videoType = mediaLibrary.MediaType.VIDEO;
let videoFetchOp = {
selections: fileKeyObj.MEDIA_TYPE + '= ?',
selectionArgs: [videoType.toString()],
}
let AlbumNoArgsFetchOp = {
selections: fileKeyObj.ALBUM_NAME + '= ?',
selectionArgs: ['新建相册1']
}
let albumList = await media.getAlbums(AlbumNoArgsFetchOp);
if (albumList.length > 0) {
const album = albumList[0];
let fetchFileResult = await album.getFileAssets(videoFetchOp);
let count = fetchFileResult.getCount();
console.info("get mediaLibrary VIDEO number", count);
} else {
console.info('getAlbum list is: 0');
}
}
```
## 获取图片/视频缩略图
通过接口[FileAsset.getThumbnail](../reference/apis/js-apis-medialibrary.md#getthumbnail8-2),传入缩略图尺寸,可以获取图片/视频缩略图。缩略图常用于UI界面展示。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读权限“ohos.permission.READ_MEDIA”。
### 获取某张图片的缩略图
当需要在相册展示图片/视频、编辑预览,应用需要获取某张图片的缩略图。
下面以获取相册第一张图片的缩略图为例,缩略图尺寸为720*720。
**开发步骤**
1. 建立检索条件,用于获取目的相册下的图片资源。
2. 调用getFileAssets获取目标图片资源。
3. 调用getFirstObject获取第一张图片。
4. 调用getThumbnail获取相册中图片的缩略图。
```ts
async function getFirstThumbnailPromise() {
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
let fileKeyObj = mediaLibrary.FileKey;
let imageType = mediaLibrary.MediaType.IMAGE;
let imagesFetchOp = {
selections: fileKeyObj.MEDIA_TYPE + '= ?',
selectionArgs: [imageType.toString()],
}
let size = { width: 720, height: 720 };
const fetchFileResult = await media.getFileAssets(imagesFetchOp);
if (fetchFileResult === undefined) {
console.error("get image failed with error");
return;
} else {
const asset = await fetchFileResult.getFirstObject();
asset.getThumbnail(size).then((pixelMap) => {
pixelMap.getImageInfo().then((info) => {
console.info('get Thumbnail info: ' + "width: " + info.size.width + " height: " + info.size.height);
}).catch((err) => {
console.error("getImageInfo failed with error: " + err);
});
}).catch((err) => {
console.error("getImageInfo failed with error: " + err);
});
}
}
```
## 创建媒体资源
通过接口[MediaLibrary.createAsset](../reference/apis/js-apis-medialibrary.md#createasset8-1)可以创建媒体资源。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读写权限“ohos.permission.WRITE_MEDIA”。
- [获取公共目录路径](medialibrary-filepath-guidelines.md)
下面以创建文件类型(MediaType.FILE)的文件为例。
```ts
async function example() {
let mediaType = mediaLibrary.MediaType.FILE;
let DIR_DOCUMENTS = mediaLibrary.DirectoryType.DIR_DOCUMENTS;
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
const path = await media.getPublicDirectory(DIR_DOCUMENTS);
media.createAsset(mediaType, "testFile.text", path).then((asset) => {
console.info("createAsset successfully:"+ JSON.stringify(asset));
}).catch((err) => {
console.error("createAsset failed with error: " + err);
});
}
```
## 将文件放入回收站
通过[FileAsset.trash](../reference/apis/js-apis-medialibrary.md#trash8)可以将文件放入回收站。
放入回收站的文件将会保存30天,在此期间,开发者可以将trash的入参isTrash设置为false将其恢复为正常文件;应用用户也可以通过系统应用“文件管理”或“图库”恢复文件。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读写权限“ohos.permission.WRITE_MEDIA”。
下面以将文件检索结果中第一个文件放入回收站为例。
**开发步骤**
1. 建立检索条件,用于获取目的相册下的图片资源。
2. 调用getFileAssets获取目标图片资源。
3. 调用getFirstObject获取第一张图片,即要放入回收站的图片对象。
4. 调用trash将文件放入回收站。
```ts
async function example() {
let fileKeyObj = mediaLibrary.FileKey;
let fileType = mediaLibrary.MediaType.FILE;
let option = {
selections: fileKeyObj.MEDIA_TYPE + '= ?',
selectionArgs: [fileType.toString()],
};
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
const fetchFileResult = await media.getFileAssets(option);
let asset = await fetchFileResult.getFirstObject();
if (asset === undefined) {
console.error('asset not exist');
return;
}
//回调为空
asset.trash(true).then(() => {
console.info("trash successfully");
}).catch((err) => {
console.error("trash failed with error: " + err);
});
}
```
## 重命名媒体资源
重命名修改的是文件的FileAsset.displayName属性,即文件的显示文件名,包含文件后缀。
修改后再通过[FileAsset.commitModify](../reference/apis/js-apis-medialibrary.md#commitmodify8-1)更新到数据库中。
在重命名文件之前,需要先获取文件对象,可以通过[FetchFileResult](../reference/apis/js-apis-medialibrary.md#fetchfileresult7)中的接口获取对应位置的文件。
**前提条件**
- 获取媒体库mediaLibrary实例。
- 申请媒体库读写权限“ohos.permission.WRITE_MEDIA”。
下面以将文件检索结果中第一个文件重命名为“newImage.jpg”为例。
**开发步骤**
1. 建立检索条件,用于获取目的相册下的图片资源。
2. 调用getFileAssets获取目标图片资源。
3. 调用getFirstObject获取第一张图片,即要重命名的图片对象。
4. 将图片重命名为“newImage.jpg“。
5. 调用FileAsset.commitModify将更新的图片属性修改到数据库中。
```ts
async function example() {
let fileKeyObj = mediaLibrary.FileKey;
let fileType = mediaLibrary.MediaType.IMAGE;
let option = {
selections: fileKeyObj.MEDIA_TYPE + '= ?',
selectionArgs: [fileType.toString()],
};
const context = getContext(this);
let media = mediaLibrary.getMediaLibrary(context);
const fetchFileResult = await media.getFileAssets(option);
let asset = await fetchFileResult.getFirstObject();
if (asset === undefined) {
console.error('asset not exist');
return;
}
asset.displayName = 'newImage.jpg';
//回调为空
asset.commitModify((err) => {
if (err) {
console.error('fileRename Failed ');
return;
}
console.info('fileRename successful.');
});
}
```
# 保存用户文件
在从网络下载文件到本地、或将已有用户文件另存为新的文件路径等场景下,需要使用FilePicker提供的保存用户文件的能力。
对音频、图片、视频、文档类文件的保存操作类似,均通过调用对应picker的save()接口并传入对应的saveOptions来实现。
## 保存图片或视频类文件
1. 导入选择器模块。
```ts
import picker from '@ohos.file.picker';
```
2. 创建图库保存选项实例。
```ts
const photoSaveOptions = new picker.PhotoSaveOptions(); // 创建文件管理器保存选项实例
photoSaveOptions.newFileNames = ["PhotoViewPicker01.jpg"]; // 保存文件名(可选)
```
3. 创建图库选择器实例,调用[save()](../reference/apis/js-apis-file-picker.md#save)接口拉起FilePicker界面进行文件保存。
用户选择目标文件夹,用户选择与文件类型相对应的文件夹,即可完成文件保存操作。保存成功后,返回保存文档的URI。
```ts
const photoViewPicker = new picker.PhotoViewPicker();
photoViewPicker.save(photoSaveOptions)
.then(async (photoSaveResult) => {
let uri = photoSaveResult[0];
// 获取到到图片或者视频文件的URI后进行文件读取等操作
})
.catch((err) => {
console.error(`Invoke documentPicker.select failed, code is ${err.code}, message is ${err.message}`);
})
```
## 保存文档类文件
1. 导入选择器模块。
```ts
import picker from '@ohos.file.picker';
```
2. 创建文档保存选项实例。
```ts
const documentSaveOptions = new picker.DocumentSaveOptions(); // 创建文件管理器选项实例
documentSaveOptions.newFileNames = ["DocumentViewPicker01.txt"]; // 保存文件名(可选)
```
3. 创建文档选择器实例。调用[save()](../reference/apis/js-apis-file-picker.md#save-3)接口拉起FilePicker界面进行文件保存。
用户选择目标文件夹,用户选择与文件类型相对应的文件夹,即可完成文件保存操作。保存成功后,返回保存文档的URI。
> **说明:**
>
> 目前DocumentSelectOptions不支持参数配置,默认可以选择所有类型的用户文件。
```ts
const documentViewPicker = new picker.DocumentViewPicker(); // 创建文件选择器实例
documentViewPicker.save(documentSaveOptions)
.then(async (documentSaveResult) => {
let uri = documentSaveResult[0];
// 例如,可以根据获取的URI进行文件写入等操作
})
.catch((err) => {
console.error(`Invoke documentPicker.save failed, code is ${err.code}, message is ${err.message}`);
})
```
## 保存音频类文件
1. 导入选择器模块。
```ts
import picker from '@ohos.file.picker';
```
2. 创建音频保存选项实例。
```ts
const audioSaveOptions = new picker.AudioSaveOptions(); // 创建文件管理器选项实例
audioSaveOptions.newFileNames = ['AudioViewPicker01.mp3']; // 保存文件名(可选)
```
3. 创建音频选择器实例。调用[save()](../reference/apis/js-apis-file-picker.md#save-6)接口拉起FilePicker界面进行文件保存。
用户选择目标文件夹,用户选择与文件类型相对应的文件夹,即可完成文件保存操作。保存成功后,返回保存文档的URI。
> **说明:**
>
> 目前AudioSelectOptions不支持参数配置,默认可以选择所有类型的用户文件。
```ts
const audioViewPicker = new picker.AudioViewPicker();
audioViewPicker.save(audioSaveOptions)
.then((audioSelectResult) => {
let uri = audioSelectResult[0];
// 获取到到音频文件的URI后进行文件读取等操作
})
.catch((err) => {
console.error(`Invoke audioPicker.select failed, code is ${err.code}, message is ${err.message}`);
})
```
# 选择用户文件
终端用户有时需要分享、保存一些图片、视频等用户文件,开发者需要在应用中支持此类使用场景。此时,开发者可以使用OpenHarmony系统预置的[文件选择器(FilePicker)](../reference/apis/js-apis-file-picker.md),实现用户文件选择及保存能力。
根据用户文件的常见类型,文件选择器(FilePicker)分别提供以下接口:
- [PhotoViewPicker](../reference/apis/js-apis-file-picker.md#photoviewpicker):适用于图片或视频类文件的选择与保存。
- [DocumentViewPicker](../reference/apis/js-apis-file-picker.md#documentviewpicker):适用于文档类文件的选择与保存。
- [AudioViewPicker](../reference/apis/js-apis-file-picker.md#audioviewpicker):适用于音频类文件的选择与保存。
## 选择图片或视频类文件
1. 导入选择器模块。
```ts
import picker from '@ohos.file.picker';
```
2. 创建图库选择选项实例。
```ts
const photoSelectOptions = new picker.PhotoSelectOptions();
```
3. 选择媒体文件类型和选择媒体文件的最大数目。
以下示例以图片选择为例,媒体文件类型请参见[PhotoViewMIMETypes](../reference/apis/js-apis-file-picker.md#photoviewmimetypes)
```ts
photoSelectOptions.MIMEType = picker.PhotoViewMIMETypes.IMAGE_TYPE; // 过滤选择媒体文件类型为IMAGE
photoSelectOptions.maxSelectNumber = 5; // 选择媒体文件的最大数目
```
4. 创建图库选择器实例,调用[select()](../reference/apis/js-apis-file-picker.md#select)接口拉起FilePicker界面进行文件选择。
文件选择成功后,返回[PhotoSelectResult](../reference/apis/js-apis-file-picker.md#photoselectresult)结果集,可以根据结果集中URI进行文件读取等操作。
```ts
const photoPicker = new picker.PhotoViewPicker();
photoPicker.select(photoSelectOptions)
.then(async (photoSelectResult) => {
let uri = photoSelectResult.photoUris[0];
// 获取到到图片或者视频文件的URI后进行文件读取等操作
})
.catch((err) => {
console.error(`Invoke documentPicker.select failed, code is ${err.code}, message is ${err.message}`);
})
```
## 选择文档类文件
1. 导入选择器模块。
```ts
import picker from '@ohos.file.picker';
```
2. 创建文档选择选项实例。
```ts
const documentSelectOptions = new picker.DocumentSelectOptions();
```
3. 创建文档选择器实例。调用[select()](../reference/apis/js-apis-file-picker.md#select-3)接口拉起FilePicker界面进行文件选择。
文件选择成功后,返回被选中文档的URI结果集。开发者可以根据结果集中URI做进一步的处理。
> **说明:**
>
> 目前DocumentSelectOptions不支持参数配置,默认可以选择所有类型的用户文件。
```ts
const documentViewPicker = new picker.DocumentViewPicker(); // 创建文件选择器实例
documentViewPicker.select(documentSelectOptions)
.then((documentSelectResult) => {
let uri = documentSelectResult[0];
// 获取到到文档文件的URI后进行文件读取等操作
})
.catch((err) => {
console.error(`Invoke documentPicker.select failed, code is ${err.code}, message is ${err.message}`);
})
```
## 选择音频类文件
1. 导入选择器模块。
```ts
import picker from '@ohos.file.picker';
```
2. 创建音频选择选项实例。
```ts
const audioSelectOptions = new picker.AudioSelectOptions();
```
3. 创建音频选择器实例。调用[select()](../reference/apis/js-apis-file-picker.md#select-6)接口拉起FilePicker界面进行文件选择。
文件选择成功后,返回被选中音频的URI结果集。开发者可以根据结果集中URI做进一步的处理。
例如通过[文件管理接口](../reference/apis/js-apis-file-fs.md#ohosfilefs-%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86)根据URI拿到音频资源的文件句柄(FD),再配合媒体服务实现音频播放的开发,具体请参考[音频播放开发指导](../media/audio-playback-overview.md)。
> **说明:**
>
> 目前AudioSelectOptions不支持参数配置,默认可以选择所有类型的用户文件。
```ts
const audioViewPicker = new picker.AudioViewPicker();
audioViewPicker.select(audioSelectOptions)
.then(audioSelectResult => {
let uri = audioSelectOptions[0];
// 获取到到音频文件的URI后进行文件读取等操作
})
.catch((err) => {
console.error(`Invoke audioPicker.select failed, code is ${err.code}, message is ${err.message}`);
})
```
# 向应用沙箱推送文件
开发者在应用开发调试时,可能需要向应用沙箱下推送一些文件以期望在应用内访问或测试,此时有两种方式:
1. 可以通过DevEco Studio向应用安装路径中放入目标文件,详见[应用安装资源访问](../quick-start/resource-categories-and-access.md#资源访问)
2. 在具备设备环境时,可以使用另一种更为灵活的方式,通过hdc工具来向设备中应用沙箱路径推送文件。即本文介绍的内容。
但是hdc shell看到的调试进程下的文件路径与应用视角的应用沙箱路径不同,开发者需要先了解如下路径映射关系。
## 应用沙箱路径和调试进程视角下的真实物理路径
在应用沙箱路径下读写文件,经过映射转换,实际读写的是在hdc进程视角下看到真实物理路径中的应用文件,其对应关系如下表所示。
**表1** 应用沙箱路径与真实物理路径对应关系
| 应用沙箱路径 | 调试进程(hdc)视角下的实际路径 | 说明 |
| -------- | -------- | -------- |
| /data/storage/el1/bundle | /data/app/el1/bundle/public/&lt;PACKAGENAME&gt; | 应用安装包目录 |
| /data/storage/el1/base | /data/app/el1/&lt;USERID&gt;/base/&lt;PACKAGENAME&gt; | 应用el1级别加密数据目录 |
| /data/storage/el2/base | /data/app/el2/&lt;USERID&gt;/base/&lt;PACKAGENAME&gt; | 应用el2级别加密数据目录 |
| /data/storage/el1/database | /data/app/el1/&lt;USERID&gt;/database/&lt;PACKAGENAME&gt; | 应用el1级别加密数据库目录 |
| /data/storage/el2/database | /data/app/el2/&lt;USERID&gt;/database/&lt;PACKAGENAME&gt; | 应用el2级别加密数据库目录 |
| /data/storage/el2/distributedfiles | /mnt/hmdfs/&lt;USERID&gt;/account/merge_view/data/&lt;PACKAGENAME&gt; | 应用el2加密级别有帐号分布式数据融合目录 |
## 开发示例
以应用包com.ohos.example为例,如果是在example的应用沙箱路径“/data/storage/el1/bundle”下读写文件,从上表可知,对应的真实物理路径为“/data/app/el1/bundle/public/&lt;PACKAGENAME&gt;”,即“/data/app/el1/bundle/public/com.ohos.example”。
推送命令示例如下:
```
hdc file send ${待推送文件的本地路径} /data/app/el1/bundle/public/com.ohos.example/
```
## 切换应用沙箱视角
在调试过程中,如果权限不对或文件不存在,开发者需要从调试进程视角切换为应用视角,以便直观分析权限及文件目录问题。视角切换命令如下:
```
hdc shell // 进入shell
ps -ef|grep [hapName] // 通过ps命令找到对应应用的pid
nsenter -t [hapPid] -m /bin/sh // 通过上一步找到的应用pid进入对应应用的沙箱环境中
```
执行完成后,即切换到了应用视角,该视角下的目录路径为应用沙箱路径,可以去排查沙箱路径相关问题。
# 设置分布式文件数据等级
不同设备本身的安全能力差异较大,一些小的嵌入式设备安全能力远弱于平板等设备类型。用户或者应用不同的文件数据有不同安全诉求,例如个人的健康信息和银行卡信息等不期望被弱设备读取。因此,OpenHarmony提供一套完整的数据分级、设备分级标准,并针对不同设备制定不同的数据流转策略,具体规则请参见[数据、设备安全分级](../database/access-control-by-device-and-data-level.md)
## 接口说明
API详细介绍请参见[ohos.file.securityLabel](../reference/apis/js-apis-file-securityLabel.md)
**表1** 设置文件数据等级
| 接口名 | 功能 | 接口类型 | 支持同步 | 支持异步 |
| -------- | -------- | -------- | -------- | -------- |
| setSecurityLabel | 设置文件安全标签 | 方法 | √ | √ |
| getSecurityLabel | 获取文件安全标签 | 方法 | √ | √ |
> **须知:**
> 1. 对于不满足安全等级的文件,跨设备仍然可以看到该文件,但是无权限打开访问该文件。
>
> 2. 分布式文件系统的数据等级默认为S3,应用可以主动设置文件的安全等级。
## 开发示例
获取通用文件沙箱路径,并设置数据等级标签。示例中的context的获取方式请参见[获取UIAbility的上下文信息](../application-models/uiability-usage.md#获取uiability的上下文信息)
```ts
import securityLabel from '@ohos.file.securityLabel';
// 获取需要设备数据等级的文件沙箱路径
let context = ...; // 获取UIAbilityContext信息
let pathDir = context.filesDir;
let filePath = pathDir + '/test.txt';
// 设置文件的数据等级为s0
securityLabel.setSecurityLabel(filePath, 's0').then(() => {
console.info('Succeeded in setSecurityLabeling.');
}).catch((err) => {
console.error(`Failed to setSecurityLabel. Code: ${err.code}, message: ${err.message}`);
});
```
# 应用文件分享
应用文件分享是应用之间通过分享URI(Uniform Resource Identifier)或文件描述符FD(File Descriptor)的方式,进行文件共享的过程。由于FD分享的文件关闭FD后,无法再打开分享文件,因此不推荐使用,本文重点介绍URI分享方式。
- 基于URI分享方式,应用可分享单个文件,通过[ohos.app.ability.wantConstant](../reference/apis/js-apis-app-ability-wantConstant.md#wantconstantflags)的wantConstant.Flags接口以只读或读写权限授权给其他应用。应用可通过[ohos.file.fs](../reference/apis/js-apis-file-fs.md#fsopen)的open()接口打开URI,并进行读写操作。当前OpenHarmony API 9仅支持临时授权,分享给其他应用的文件在被分享应用退出时权限被收回。
- 基于FD分享方式,应用可分享单个文件,通过ohos.file.fs的open接口以指定权限授权给其他应用。应用从Want中解析拿到FD后可通过ohos.file.fs的读写接口对文件进行读写。
开发者可以使用相关接口,[分享文件给其他应用](#分享文件给其他应用)[使用其他应用分享的文件](#使用其他应用分享的文件)
## 文件URI规范
文件URI的格式为:
格式为file://&lt;bundleName&gt;/&lt;path&gt;/\#networkid=&lt;networkid&gt;
- file:文件URI的标志。
- bundleName:该文件资源的属主。
- path:文件资源在应用沙箱中的路径。
- networkid:为可选项,用于分布式文件系统标志该文件资源所归属的设备;当不需要区分文件位置时,该选项可不填写。
## 分享文件给其他应用
在分享文件给其他应用前,开发者需要先[获取应用文件路径](../application-models/application-context-stage.md#获取应用开发路径)
1. 获取文件在应用沙箱中的路径,并转换为文件URI。
```ts
import UIAbility from '@ohos.app.ability.UIAbility';
import fileuri from '@ohos.file.fileuri';
import window from '@ohos.window';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage) {
// 获取文件的沙箱路径
let pathInSandbox = this.context.filesDir + "/test.txt";
// 将沙箱路径转换为uri
let uri = fileuri.getUriFromPath(pathInSandbox);
// 获取的uri为"file://com.example.demo/data/storage/el2/base/files/test.txt"
}
}
```
2. 设置获取文件的权限以及选择要分享的应用。
分享文件给其他应用需要使用[startAbility()](../reference/apis/js-apis-inner-application-uiAbilityContext.md#uiabilitycontextstartability)接口,将获取到的URI填充在want的参数uri中,标注URI的文件类型,type字段可参考[Want属性](../reference/apis/js-apis-app-ability-want.md#属性),并通过设置want的flag来设置对应的读写权限,action字段配置为wantConstant.Action.ACTION_SEND_DATA表示进行应用文件分享,开发示例如下。
> **说明:**
>
> 写权限分享时,同时授予读权限。
```ts
import fileuri from '@ohos.file.fileuri';
import window from '@ohos.window';
import wantConstant from '@ohos.app.ability.wantConstant';
import UIAbility from '@ohos.app.ability.UIAbility';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage) {
// 获取文件沙箱路径
let filePath = this.context.filesDir + '/test.txt';
// 将沙箱路径转换为uri
let uri = fileuri.getUriFromPath(filePath);
let want = {
// 配置被分享文件的读写权限,例如对被分享应用进行读写授权
flags: wantConstant.Flags.FLAG_AUTH_WRITE_URI_PERMISSION | wantConstant.Flags.FLAG_AUTH_READ_URI_PERMISSION,
// 配置分享应用的隐式拉起规则
action: 'ohos.want.action.sendData',
uri: uri,
type: 'text/plain'
}
this.context.startAbility(want)
.then(() => {
console.info('Invoke getCurrentBundleStats succeeded.');
})
.catch((err) => {
console.error(`Invoke startAbility failed, code is ${err.code}, message is ${err.message}`);
});
}
...
}
```
## 使用其他应用分享的文件
被分享应用需要在[module.json5配置文件](../quick-start/module-configuration-file.md)的actions标签的值配置为"ohos.want.action.sendData",表示接收应用分享文件,配置uris字段,表示接收URI的类型,即只接收其他应用分享该类型的URI,如下表示本应用只接收scheme为file,类型为txt的文件,示例如下。
```json
{
"module": {
...
"abilities": [
{
...
"skills": [
{
...
"actions": [
"ohos.want.action.sendData"
],
"uris": [
{
"scheme": "file",
"type": "text/plain"
}
]
}
]
}
]
}
}
```
被分享方的UIAbility被启动后,可以在其[onCreate()](../reference/apis/js-apis-app-ability-uiAbility.md#uiabilityoncreate)或者[onNewWant()](../reference/apis/js-apis-app-ability-uiAbility.md#uiabilityonnewwant)回调中获取传入的Want参数信息。
通过接口want的参数获取分享文件的URI,获取文件URI后通过fs.open()接口打开文件,获取对应的file对象后,可对文件进行读写操作。
```ts
// xxx.ets
import fs from '@ohos.file.fs';
function getShareFile() {
try {
let want = ...; // 获取分享方传递过来的want信息
// 从want信息中获取uri字段
let uri = want.uri;
if (uri == null || uri == undefined) {
console.info('uri is invalid');
return;
}
try {
// 根据需要对被分享文件的URI进行相应操作。例如读写的方式打开URI获取file对象
let file = fs.openSync(uri, fs.OpenMode.READ_WRITE);
console.info('open file successfully!');
} catch (error) {
console.error(`Invoke openSync failed, code is ${error.code}, message is ${error.message}`);
}
} catch (error) {
console.error(`Invoke openSync failed, code is ${error.code}, message is ${error.message}`);
}
}
```
# 用户文件概述
用户文件:文件所有者为登录到该终端设备的用户,包括用户私有的图片、视频、音频、文档等。
1. 用户文件存放在用户目录下,归属于该设备上登录的用户。
2. 用户文件存储位置主要分为[内置存储](#内置存储)[外置存储](#外置存储)
3. 应用对用户文件的创建、访问、删除等行为,需要提前获取用户授权,或由用户操作完成。
OpenHarmony提供[用户文件访问框架](#用户文件访问框架),用于开发者访问和管理用户文件,将在下文详细介绍。
## 用户文件存储位置
### 内置存储
内置存储,是指用户文件存储在终端设备的内部存储设备(空间)上。内置存储设备无法被移除。内置存储的用户文件主要有:
- 用户特有的文件:这部分文件归属于登录该设备的用户,不同用户登录后,仅可看到该用户自己的文件。
按照这些文件的特征/属性,以及用户/应用的使用习惯,可分为:
- 图片/视频类媒体文件
所具有的特征包括拍摄时间、地点、旋转角度、文件宽高等信息,以媒体文件的形式存储在系统中,通常是以所有文件、相册的形式对外呈现,不会展示其在系统中存储的具体位置。
- 音频类媒体文件
所具有的特征包括所属专辑、音频创作者、持续时间等信息,以媒体文件的形式存储在系统中,通常会以所有文件、专辑、作家等形式对外部呈现,不会展示其在系统中存储的具体位置。
- 其他文件(统称为文档类文件)
以普通文件的形式存储在系统中,该类文件既包括普通的文本文件、压缩文件等,又包括以普通文件形式存储的图片/视频、音频文件,该类文件通常是以目录树的形式对外展示。
- 多用户共享的文件:用户可以通过将文件放在共享文件区,实现多个用户之间文件的共享访问。
共享文件区的文件,也是以普通文件的形式存储在系统中,以目录树的形式对外展示。
### 外置存储
外置存储,是指用户文件存储在外置可插拔设备上(如SD卡、U盘等)。外置存储设备上的文件,和内置存储设备共享区文件一样,可以被所有登录到系统中的用户看到。
外置存储设备具备可插拔属性,因此系统提供了设备插拔事件的监听及挂载功能,用于管理外置存储设备,具体可参考[管理外置存储设备(仅对系统应用开放)](manage-external-storage.md)
外置存储设备上的文件,全部以普通文件的形式呈现,和内置存储设备上的文档类文件一样,采用目录树的形式对外展示。
## 用户文件访问框架
用户文件访问框架(File Access Framework)是一套提供给开发者访问和管理用户文件的基础框架。该框架依托于OpenHarmony的ExtensionAbility组件机制,提供了一套统一访问用户文件的方法和接口。
**图1** 用户文件访问框架示意图  
![User file access framework](figures/user-file-access-framework.png)
- 各类系统应用或三方应用(即图中的文件访问客户端)若需访问用户文件,如选择一张照片或保存多个文档等,可以通过拉起“文件选择器应用”来实现。
- OpenHarmony系统预置了文件选择器应用FilePicker和文件管理器应用FileManager。
- FilePicker:系统预置应用,提供文件访问客户端选择和保存文件的能力,且不需要配置任何权限。FilePicker的使用指导请参见[选择用户文件](select-user-file.md)
- FileManager:系统预置应用,终端用户可通过系统文件管理器实现查看文件、修改文件、删除文件(目录)、重命名文件(目录)、移动文件(目录)、创建文件(目录)等操作。
对于系统应用开发者,还可以按需开发自己的文件选择器或文件管理器应用。其中,选择器功能是管理器的子集,本文目前提供了管理器的开发指导,请参见[开发用户文件管理器(仅对系统应用开放)](dev-user-file-manager.md)
- File Access Framework(用户文件访问框架)的主要功能模块如下:
- File Access Helper:提供给文件管理器和文件选择器访问用户文件的API接口。
- File Access ExtensionAbility:提供文件访问框架能力,由内卡文件管理服务UserFileManager和外卡文件管理服务ExternalFileManager组成,实现对应的文件访问功能。
- UserFileManager:内卡文件管理服务,基于File Access ExtensionAbility框架实现,用于管理内置存储设备上的文件。
- ExternalFileManager:外卡文件管理服务,基于File Access ExtensionAbility框架实现,用于管理外置存储设备上的文件。
# 媒体
- [媒体应用开发概述](media-application-overview.md)
- 音视频
- [音频开发概述](audio-overview.md)
- [音频渲染开发指导](audio-renderer.md)
- [音频流管理开发指导](audio-stream-manager.md)
- [音频采集开发指导](audio-capturer.md)
- [OpenSL ES播放开发指导](opensles-playback.md)
- [OpenSL ES录音开发指导](opensles-capture.md)
- [音频焦点模式开发指导](audio-interruptmode.md)
- [音量管理开发指导](audio-volume-manager.md)
- [路由、设备管理开发指导](audio-routing-manager.md)
- [音视频播放器开发指导(推荐使用)](avplayer-playback.md)
- [音视频录制开发指导(推荐使用)](avrecorder.md)
- [音频播放开发指导(待停用)](audio-playback.md)
- [音频录制开发指导(待停用)](audio-recorder.md)
- [视频播放开发指导(待停用)](video-playback.md)
- [视频录制开发指导(待停用)](video-recorder.md)
- 媒体会话
- [AVSession开发概述](avsession-overview.md)
- [AVSession开发指导](avsession-guidelines.md)
- [音视频概述](av-overview.md)
- [AVPlayer和AVRecorder](avplayer-avrecorder-overview.md)
- 音频播放
- [音频播放开发概述](audio-playback-overview.md)
- [使用AVPlayer开发音频播放功能](using-avplayer-for-playback.md)
- [使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md)
- [使用OpenSL ES开发音频播放功能](using-opensl-es-for-playback.md)
- [使用TonePlayer开发音频播放功能(仅对系统应用开放)](using-toneplayer-for-playback.md)
- [多音频播放的并发策略](audio-playback-concurrency.md)
- [播放音量管理](volume-management.md)
- [音频播放流管理](audio-playback-stream-management.md)
- [音频输出设备管理](audio-output-device-management.md)
- [分布式音频播放(仅对系统应用开放)](distributed-audio-playback.md)
- 音频录制
- [音频录制开发概述](audio-recording-overview.md)
- [使用AVRecorder开发音频录制功能](using-avrecorder-for-recording.md)
- [使用AudioCapturer开发音频录制功能](using-audiocapturer-for-recording.md)
- [使用OpenSLES开发音频录制功能](using-opensl-es-for-recording.md)
- [管理麦克风](mic-management.md)
- [音频录制流管理](audio-recording-stream-management.md)
- [音频输入设备管理](audio-input-device-management.md)
- 音频通话
- [音频通话开发概述](audio-call-overview.md)
- [开发音频通话功能](audio-call-development.md)
- [视频播放](video-playback.md)
- [视频录制](video-recording.md)
- 媒体会话(仅对系统应用开放)
- [媒体会话概述](avsession-overview.md)
- 本地媒体会话
- [本地媒体会话概述](local-avsession-overview.md)
- [媒体会话提供方](using-avsession-developer.md)
- [媒体会话控制方](using-avsession-controller.md)
- 分布式媒体会话
- [分布式媒体会话概述](distributed-avsession-overview.md)
- [使用分布式媒体会话](using-distributed-avsession.md)
- 相机(仅对系统应用开放)
- [相机开发概述](camera-overview.md)
- 相机开发指导
- [开发准备](camera-preparation.md)
- [设备输入](camera-device-input.md)
- [会话管理](camera-session-management.md)
- [预览](camera-preview.md)
- [拍照](camera-shooting.md)
- [录像](camera-recording.md)
- [元数据](camera-metadata.md)
- 相机最佳实践
- [拍照实现方案](camera-shooting-case.md)
- [录像实现方案](camera-recording-case.md)
- 图片
- [图片开发指导](image.md)
- 相机
- [相机开发指导](camera.md)
- [分布式相机开发指导](remote-camera.md)
\ No newline at end of file
- [图片开发概述](image-overview.md)
- [图片解码](image-decoding.md)
- 图片处理
- [图像变换](image-transformation.md)
- [位图操作](image-pixelmap-operation.md)
- [图片编码](image-encoding.md)
- [图片工具](image-tool.md)
# 开发音频通话功能
在音频通话场景下,音频输出(播放对端声音)和音频输入(录制本端声音)会同时进行,应用可以通过使用AudioRenderer来实现音频输出,通过使用AudioCapturer来实现音频输入,同时使用AudioRenderer和AudioCapturer即可实现音频通话功能。
在音频通话开始和结束时,应用可以自行检查当前的[音频场景模式](audio-call-overview.md#音频场景模式)[铃声模式](audio-call-overview.md#铃声模式),以便采取合适的音频管理及提示策略。
以下代码示范了同时使用AudioRenderer和AudioCapturer实现音频通话功能的基本过程,其中未包含音频通话数据的传输过程,实际开发中,需要将网络传输来的对端通话数据解码播放,此处仅以读取音频文件的数据代替;同时需要将本端录制的通话数据编码打包,通过网络发送给对端,此处仅以将数据写入音频文件代替。
## 使用AudioRenderer播放对端的通话声音
该过程与[使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md)过程相似,关键区别在于audioRenderInfo参数和音频数据来源。audioRenderInfo参数中,音频内容类型需设置为语音,CONTENT_TYPE_SPEECH,音频流使用类型需设置为语音通信,STREAM_USAGE_VOICE_COMMUNICATION。
```ts
import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs';
const TAG = 'VoiceCallDemoForAudioRenderer';
// 与使用AudioRenderer开发音频播放功能过程相似,关键区别在于audioRenderInfo参数和音频数据来源
export default class VoiceCallDemoForAudioRenderer {
private renderModel = undefined;
private audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率
channels: audio.AudioChannel.CHANNEL_2, // 通道数
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式
}
private audioRendererInfo = {
// 需使用通话场景相应的参数
content: audio.ContentType.CONTENT_TYPE_SPEECH, // 音频内容类型:语音
usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION, // 音频流使用类型:语音通信
rendererFlags: 0 // 音频渲染器标志:默认为0即可
}
private audioRendererOptions = {
streamInfo: this.audioStreamInfo,
rendererInfo: this.audioRendererInfo
}
// 初始化,创建实例,设置监听事件
init() {
audio.createAudioRenderer(this.audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例
if (!err) {
console.info(`${TAG}: creating AudioRenderer success`);
this.renderModel = renderer;
this.renderModel.on('stateChange', (state) => { // 设置监听事件,当转换到指定的状态时触发回调
if (state == 1) {
console.info('audio renderer state is: STATE_PREPARED');
}
if (state == 2) {
console.info('audio renderer state is: STATE_RUNNING');
}
});
this.renderModel.on('markReach', 1000, (position) => { // 订阅markReach事件,当渲染的帧数达到1000帧时触发回调
if (position == 1000) {
console.info('ON Triggered successfully');
}
});
} else {
console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`);
}
});
}
// 开始一次音频渲染
async start() {
let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
if (stateGroup.indexOf(this.renderModel.state) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染
console.error(TAG + 'start failed');
return;
}
await this.renderModel.start(); // 启动渲染
const bufferSize = await this.renderModel.getBufferSize();
// 此处仅以读取音频文件的数据举例,实际音频通话开发中,需要读取的是通话对端传输来的音频数据
let context = getContext(this);
let path = context.filesDir;
const filePath = path + '/voice_call_data.wav'; // 沙箱路径,实际路径为/data/storage/el2/base/haps/entry/files/voice_call_data.wav
let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
let stat = await fs.stat(filePath);
let buf = new ArrayBuffer(bufferSize);
let len = stat.size % bufferSize === 0 ? Math.floor(stat.size / bufferSize) : Math.floor(stat.size / bufferSize + 1);
for (let i = 0; i < len; i++) {
let options = {
offset: i * bufferSize,
length: bufferSize
};
let readsize = await fs.read(file.fd, buf, options);
// buf是要写入缓冲区的音频数据,在调用AudioRenderer.write()方法前可以进行音频数据的预处理,实现个性化的音频播放功能,AudioRenderer会读出写入缓冲区的音频数据进行渲染
let writeSize = await new Promise((resolve, reject) => {
this.renderModel.write(buf, (err, writeSize) => {
if (err) {
reject(err);
} else {
resolve(writeSize);
}
});
});
if (this.renderModel.state === audio.AudioState.STATE_RELEASED) { // 如果渲染器状态为released,停止渲染
fs.close(file);
await this.renderModel.stop();
}
if (this.renderModel.state === audio.AudioState.STATE_RUNNING) {
if (i === len - 1) { // 如果音频文件已经被读取完,停止渲染
fs.close(file);
await this.renderModel.stop();
}
}
}
}
// 暂停渲染
async pause() {
// 只有渲染器状态为running的时候才能暂停
if (this.renderModel.state !== audio.AudioState.STATE_RUNNING) {
console.info('Renderer is not running');
return;
}
await this.renderModel.pause(); // 暂停渲染
if (this.renderModel.state === audio.AudioState.STATE_PAUSED) {
console.info('Renderer is paused.');
} else {
console.error('Pausing renderer failed.');
}
}
// 停止渲染
async stop() {
// 只有渲染器状态为running或paused的时候才可以停止
if (this.renderModel.state !== audio.AudioState.STATE_RUNNING && this.renderModel.state !== audio.AudioState.STATE_PAUSED) {
console.info('Renderer is not running or paused.');
return;
}
await this.renderModel.stop(); // 停止渲染
if (this.renderModel.state === audio.AudioState.STATE_STOPPED) {
console.info('Renderer stopped.');
} else {
console.error('Stopping renderer failed.');
}
}
// 销毁实例,释放资源
async release() {
// 渲染器状态不是released状态,才能release
if (this.renderModel.state === audio.AudioState.STATE_RELEASED) {
console.info('Renderer already released');
return;
}
await this.renderModel.release(); // 释放资源
if (this.renderModel.state === audio.AudioState.STATE_RELEASED) {
console.info('Renderer released');
} else {
console.error('Renderer release failed.');
}
}
}
```
## 使用AudioCapturer录制本端的通话声音
该过程与[使用AudioCapturer开发音频录制功能](using-audiocapturer-for-recording.md)过程相似,关键区别在于audioCapturerInfo参数和音频数据流向。audioCapturerInfo参数中音源类型需设置为语音通话,SOURCE_TYPE_VOICE_COMMUNICATION。
```ts
import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs';
const TAG = 'VoiceCallDemoForAudioCapturer';
// 与使用AudioCapturer开发音频录制功能过程相似,关键区别在于audioCapturerInfo参数和音频数据流向
export default class VoiceCallDemoForAudioCapturer {
private audioCapturer = undefined;
private audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100, // 采样率
channels: audio.AudioChannel.CHANNEL_1, // 通道数
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式
}
private audioCapturerInfo = {
// 需使用通话场景相应的参数
source: audio.SourceType.SOURCE_TYPE_VOICE_COMMUNICATION, // 音源类型:语音通话
capturerFlags: 0 // 音频采集器标志:默认为0即可
}
private audioCapturerOptions = {
streamInfo: this.audioStreamInfo,
capturerInfo: this.audioCapturerInfo
}
// 初始化,创建实例,设置监听事件
init() {
audio.createAudioCapturer(this.audioCapturerOptions, (err, capturer) => { // 创建AudioCapturer实例
if (err) {
console.error(`Invoke createAudioCapturer failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info(`${TAG}: create AudioCapturer success`);
this.audioCapturer = capturer;
this.audioCapturer.on('markReach', 1000, (position) => { // 订阅markReach事件,当采集的帧数达到1000时触发回调
if (position === 1000) {
console.info('ON Triggered successfully');
}
});
this.audioCapturer.on('periodReach', 2000, (position) => { // 订阅periodReach事件,当采集的帧数达到2000时触发回调
if (position === 2000) {
console.info('ON Triggered successfully');
}
});
});
}
// 开始一次音频采集
async start() {
let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
if (stateGroup.indexOf(this.audioCapturer.state) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动采集
console.error(`${TAG}: start failed`);
return;
}
await this.audioCapturer.start(); // 启动采集
// 此处仅以将音频数据写入文件举例,实际音频通话开发中,需要将本端采集的音频数据编码打包,通过网络发送给通话对端
let context = getContext(this);
const path = context.filesDir + '/voice_call_data.wav'; // 采集到的音频文件存储路径
let file = fs.openSync(path, 0o2 | 0o100); // 如果文件不存在则创建文件
let fd = file.fd;
let numBuffersToCapture = 150; // 循环写入150次
let count = 0;
while (numBuffersToCapture) {
let bufferSize = await this.audioCapturer.getBufferSize();
let buffer = await this.audioCapturer.read(bufferSize, true);
let options = {
offset: count * bufferSize,
length: bufferSize
};
if (buffer === undefined) {
console.error(`${TAG}: read buffer failed`);
} else {
let number = fs.writeSync(fd, buffer, options);
console.info(`${TAG}: write date: ${number}`);
}
numBuffersToCapture--;
count++;
}
}
// 停止采集
async stop() {
// 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止
if (this.audioCapturer.state !== audio.AudioState.STATE_RUNNING && this.audioCapturer.state !== audio.AudioState.STATE_PAUSED) {
console.info('Capturer is not running or paused');
return;
}
await this.audioCapturer.stop(); // 停止采集
if (this.audioCapturer.state === audio.AudioState.STATE_STOPPED) {
console.info('Capturer stopped');
} else {
console.error('Capturer stop failed');
}
}
// 销毁实例,释放资源
async release() {
// 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release
if (this.audioCapturer.state === audio.AudioState.STATE_RELEASED || this.audioCapturer.state === audio.AudioState.STATE_NEW) {
console.info('Capturer already released');
return;
}
await this.audioCapturer.release(); // 释放资源
if (this.audioCapturer.state == audio.AudioState.STATE_RELEASED) {
console.info('Capturer released');
} else {
console.error('Capturer release failed');
}
}
}
```
# 音频通话开发概述
常用的音频通话模式包括VOIP通话和蜂窝通话。
- VOIP通话:
VOIP(Voice over Internet Protocol)通话是指基于互联网协议(IP)进行通讯的一种语音通话技术。VOIP通话会将通话信息打包成数据包,通过网络进行传输,因此VOIP通话对网络要求较高,通话质量与网络连接速度紧密相关。
- 蜂窝通话(仅对系统应用开放)
蜂窝通话是指传统的电话功能,由运营商提供服务,目前仅对系统应用开放,未向第三方应用提供开发接口。
在开发音频通话相关功能时,开发者可以根据实际情况,检查当前的[音频场景模式](#音频场景模式)[铃声模式](#铃声模式),以使用相应的音频处理策略。
## 音频场景模式
应用使用音频通话相关功能时,系统会切换至与通话相关的音频场景模式([AudioScene](../reference/apis/js-apis-audio.md#audioscene8)),当前预置了多种音频场景,包括响铃、通话、语音聊天等,在不同的场景下,系统会采用不同的策略来处理音频。
如在蜂窝通话场景中会更注重人声的清晰度。系统会使用3A算法对音频数据进行预处理,抑制通话回声,消除背景噪音,调整音量范围,从而达到清晰人声的效果。3A算法,指声学回声消除(Acoustic Echo Cancelling, AEC)、背景噪声抑制(Acitve Noise Control, ANC)、自动增益控制(Automatic Gain Control, AGC)三种音频处理算法。
当前预置的四种音频场景:
- AUDIO_SCENE_DEFAULT:默认音频场景,音频通话之外的场景均可使用。
- AUDIO_SCENE_RINGING:响铃音频场景,来电响铃时使用,仅对系统应用开放。
- AUDIO_SCENE_PHONE_CALL:蜂窝通话音频场景,蜂窝通话时使用,仅对系统应用开放。
- AUDIO_SCENE_VOICE_CHAT:语音聊天音频场景,VOIP通话时使用。
应用可通过[AudioManager](../reference/apis/js-apis-audio.md#audiomanager)的getAudioScene来获取当前的音频场景模式。当应用开始或结束使用音频通话相关功能时,可通过此方法检查系统是否已切换为合适的音频场景模式。
## 铃声模式
在用户进入到音频通话时,应用可以使用铃声或振动来提示用户。系统通过调整铃声模式([AudioRingMode](../reference/apis/js-apis-audio.md#audioringmode)),实现便捷地管理铃声音量,并调整设备的振动模式。
当前预置的三种铃声模式:
- RINGER_MODE_SILENT:静音模式,此模式下铃声音量为零(即静音)。
- RINGER_MODE_VIBRATE:振动模式,此模式下铃声音量为零,设备振动开启(即响铃时静音,触发振动)。
- RINGER_MODE_NORMAL:响铃模式,此模式下铃声音量正常。
应用可以调用[AudioVolumeGroupManager](../reference/apis/js-apis-audio.md#audiovolumegroupmanager9)中的getRingerMode获取当前的铃声模式,以便采取合适的提示策略。
如果应用希望及时获取铃声模式的变化情况,可以通过AudioVolumeGroupManager中的on('ringerModeChange')监听铃声模式变化事件,使应用在铃声模式发生变化时及时收到通知,方便应用做出相应的调整。
## 通话场景音频设备切换
在通话场景下,系统会根据默认优先级选择合适的音频设备。应用可以根据需要,自主切换音频设备。
通信设备类型([CommunicationDeviceType](../reference/apis/js-apis-audio.md#communicationdevicetype9))是系统预置的可用于通话场景的设备,应用可以使用[AudioRoutingManager](../reference/apis/js-apis-audio.md#audioroutingmanager9)的isCommunicationDeviceActive函数获取指定通信设备的激活状态,并且可以使用AudioRoutingManager的setCommunicationDevice设置通信设备的激活状态,通过激活设备来实现通话场景音频设备的切换。
# 音频采集开发指导
## 简介
AudioCapturer提供了用于获取原始音频文件的方法。开发者可以通过本指导了解应用如何通过AudioCapturer接口的调用实现音频数据的采集。
- **状态检查**:在进行应用开发的过程中,建议开发者通过on('stateChange')方法订阅AudioCapturer的状态变更。因为针对AudioCapturer的某些操作,仅在音频采集器在固定状态时才能执行。如果应用在音频采集器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
## 运作机制
该模块提供了音频采集模块的状态变化示意图。
**图1** 音频采集状态变化示意图
![audio-capturer-state](figures/audio-capturer-state.png)
**PREPARED状态:** 通过调用create()方法进入到该状态。<br>
**RUNNING状态:** 正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在stopped状态通过调用start()方法进入此状态。<br>
**STOPPED状态:** 在running状态可以通过stop()方法停止音频数据的播放。<br>
**RELEASED状态:** 在prepared和stop状态,用户均可通过release()方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。<br>
## 约束与限制
开发者在进行音频数据采集功能开发前,需要先对所开发的应用配置麦克风权限(ohos.permission.MICROPHONE),配置方式请参见[访问控制授权申请](../security/accesstoken-guidelines.md#配置文件权限声明)
## 开发指导
详细API含义可参考:[音频管理API文档AudioCapturer](../reference/apis/js-apis-audio.md#audiocapturer8)
1. 使用createAudioCapturer()创建一个全局的AudioCapturer实例。
在audioCapturerOptions中设置音频采集器的相关参数。该实例可用于音频采集、控制和获取采集状态,以及注册通知回调。
```js
import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs'; //便于步骤3 read函数调用
//音频渲染相关接口自测试
@Entry
@Component
struct AudioRenderer {
@State message: string = 'Hello World'
private audioCapturer : audio.AudioCapturer; //供全局调用
async initAudioCapturer(){
let audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
}
let audioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC,
capturerFlags: 0 // 0是音频采集器的扩展标志位,默认为0
}
let audioCapturerOptions = {
streamInfo: audioStreamInfo,
capturerInfo: audioCapturerInfo
}
this.audioCapturer = await audio.createAudioCapturer(audioCapturerOptions);
console.log('AudioRecLog: Create audio capturer success.');
}
```
2. 调用start()方法来启动/恢复采集任务。
启动完成后,采集器状态将变更为STATE_RUNNING,然后应用可以开始读取缓冲区。
```js
async startCapturer() {
let state = this.audioCapturer.state;
// Capturer start时的状态应该是STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一.
if (state == audio.AudioState.STATE_PREPARED || state == audio.AudioState.STATE_PAUSED ||
state == audio.AudioState.STATE_STOPPED) {
await this.audioCapturer.start();
state = this.audioCapturer.state;
if (state == audio.AudioState.STATE_RUNNING) {
console.info('AudioRecLog: Capturer started');
} else {
console.error('AudioRecLog: Capturer start failed');
}
}
}
```
3. 读取采集器的音频数据并将其转换为字节流。重复调用read()方法读取数据,直到应用准备停止采集。
参考以下示例,将采集到的数据写入文件。
```js
async readData(){
let state = this.audioCapturer.state;
// 只有状态为STATE_RUNNING的时候才可以read.
if (state != audio.AudioState.STATE_RUNNING) {
console.info('Capturer is not in a correct state to read');
return;
}
const path = '/data/data/.pulse_dir/capture_js.wav'; // 采集到的音频文件存储路径
let file = fs.openSync(path, 0o2);
let fd = file.fd;
if (file !== null) {
console.info('AudioRecLog: file created');
} else {
console.info('AudioRecLog: file create : FAILED');
return;
}
if (fd !== null) {
console.info('AudioRecLog: file fd opened in append mode');
}
let numBuffersToCapture = 150; // 循环写入150次
let count = 0;
while (numBuffersToCapture) {
this.bufferSize = await this.audioCapturer.getBufferSize();
let buffer = await this.audioCapturer.read(this.bufferSize, true);
let options = {
offset: count * this.bufferSize,
length: this.bufferSize
}
if (typeof(buffer) == undefined) {
console.info('AudioRecLog: read buffer failed');
} else {
let number = fs.writeSync(fd, buffer, options);
console.info(`AudioRecLog: data written: ${number}`);
}
numBuffersToCapture--;
count++;
}
}
```
4. 采集完成后,调用stop方法,停止录制。
```js
async StopCapturer() {
let state = this.audioCapturer.state;
// 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止
if (state != audio.AudioState.STATE_RUNNING && state != audio.AudioState.STATE_PAUSED) {
console.info('AudioRecLog: Capturer is not running or paused');
return;
}
await this.audioCapturer.stop();
state = this.audioCapturer.state;
if (state == audio.AudioState.STATE_STOPPED) {
console.info('AudioRecLog: Capturer stopped');
} else {
console.error('AudioRecLog: Capturer stop failed');
}
}
```
5. 任务结束,调用release()方法释放相关资源。
```js
async releaseCapturer() {
let state = this.audioCapturer.state;
// 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release
if (state == audio.AudioState.STATE_RELEASED || state == audio.AudioState.STATE_NEW) {
console.info('AudioRecLog: Capturer already released');
return;
}
await this.audioCapturer.release();
state = this.audioCapturer.state;
if (state == audio.AudioState.STATE_RELEASED) {
console.info('AudioRecLog: Capturer released');
} else {
console.info('AudioRecLog: Capturer release failed');
}
}
```
6. (可选)获取采集器相关信息
通过以下代码,可以获取采集器的相关信息。
```js
async getAudioCapturerInfo(){
// 获取当前采集器状态
let state = this.audioCapturer.state;
// 获取采集器信息
let audioCapturerInfo : audio.AudioCapturerInfo = await this.audioCapturer.getCapturerInfo();
// 获取音频流信息
let audioStreamInfo : audio.AudioStreamInfo = await this.audioCapturer.getStreamInfo();
// 获取音频流ID
let audioStreamId : number = await this.audioCapturer.getAudioStreamId();
// 获取纳秒形式的Unix时间戳
let audioTime : number = await this.audioCapturer.getAudioTime();
// 获取合理的最小缓冲区大小
let bufferSize : number = await this.audioCapturer.getBufferSize();
}
```
7. (可选)使用on('markReach')方法订阅采集器标记到达事件,使用off('markReach')取消订阅事件。
注册markReach监听后,当采集器采集的帧数到达设定值时,会触发回调并返回设定的值。
```js
async markReach(){
this.audioCapturer.on('markReach', 10, (reachNumber) => {
console.info('Mark reach event Received');
console.info(`The Capturer reached frame: ${reachNumber}`);
});
this.audioCapturer.off('markReach'); // 取消markReach事件的订阅,后续将无法监听到“标记到达”事件
}
```
8. (可选)使用on('periodReach')方法订阅采集器区间标记到达事件,使用off('periodReach')取消订阅事件。
注册periodReach监听后,**每当**采集器采集的帧数到达设定值时,会触发回调并返回设定的值。
```js
async periodReach(){
this.audioCapturer.on('periodReach', 10, (reachNumber) => {
console.info('Period reach event Received');
console.info(`In this period, the Capturer reached frame: ${reachNumber}`);
});
this.audioCapturer.off('periodReach'); // 取消periodReach事件的订阅,后续将无法监听到“区间标记到达”事件
}
```
9. 如果应用需要在采集器状态更新时进行一些操作,可以订阅该事件,当采集器状态更新时,会受到一个包含有事件类型的回调。
```js
async stateChange(){
this.audioCapturer.on('stateChange', (state) => {
console.info(`AudioCapturerLog: Changed State to : ${state}`)
switch (state) {
case audio.AudioState.STATE_PREPARED:
console.info('--------CHANGE IN AUDIO STATE----------PREPARED--------------');
console.info('Audio State is : Prepared');
break;
case audio.AudioState.STATE_RUNNING:
console.info('--------CHANGE IN AUDIO STATE----------RUNNING--------------');
console.info('Audio State is : Running');
break;
case audio.AudioState.STATE_STOPPED:
console.info('--------CHANGE IN AUDIO STATE----------STOPPED--------------');
console.info('Audio State is : stopped');
break;
case audio.AudioState.STATE_RELEASED:
console.info('--------CHANGE IN AUDIO STATE----------RELEASED--------------');
console.info('Audio State is : released');
break;
default:
console.info('--------CHANGE IN AUDIO STATE----------INVALID--------------');
console.info('Audio State is : invalid');
break;
}
});
}
```
\ No newline at end of file
# 音频输入设备管理
有时设备同时连接多个音频输入设备,需要指定音频输入设备进行音频录制,此时需要使用AudioRoutingManager接口进行输入设备的管理,API说明可以参考[AudioRoutingManager API文档](../reference/apis/js-apis-audio.md#audioroutingmanager9)
## 创建AudioRoutingManager实例
在使用AudioRoutingManager管理音频设备前,需要先导入模块并创建实例。
```ts
import audio from '@ohos.multimedia.audio'; // 导入audio模块
let audioManager = audio.getAudioManager(); // 需要先创建AudioManager实例
let audioRoutingManager = audioManager.getRoutingManager(); // 再调用AudioManager的方法创建AudioRoutingManager实例
```
## 支持的音频输入设备类型
目前支持的音频输入设备见下表:
| 名称 | 值 | 说明 |
| -------- | -------- | -------- |
| WIRED_HEADSET | 3 | 有线耳机,带麦克风。 |
| BLUETOOTH_SCO | 7 | 蓝牙设备SCO(Synchronous Connection Oriented)连接。 |
| MIC | 15 | 麦克风。 |
| USB_HEADSET | 22 | USB耳机,带麦克风。 |
## 获取输入设备信息
使用getDevices()方法可以获取当前所有输入设备的信息。
```ts
audioRoutingManager.getDevices(audio.DeviceFlag.INPUT_DEVICES_FLAG).then((data) => {
console.info('Promise returned to indicate that the device list is obtained.');
});
```
## 监听设备连接状态变化
可以设置监听事件来监听设备连接状态的变化,当有设备连接或断开时触发回调:
```ts
// 监听音频设备状态变化
audioRoutingManager.on('deviceChange', audio.DeviceFlag.INPUT_DEVICES_FLAG, (deviceChanged) => {
console.info('device change type : ' + deviceChanged.type); // 设备连接状态变化,0为连接,1为断开连接
console.info('device descriptor size : ' + deviceChanged.deviceDescriptors.length);
console.info('device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceRole); // 设备角色
console.info('device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceType); // 设备类型
});
// 取消监听音频设备状态变化
audioRoutingManager.off('deviceChange', (deviceChanged) => {
console.info('Should be no callback.');
});
```
## 选择音频输入设备(仅对系统应用开放)
选择音频输入设备,当前只能选择一个输入设备,以设备id作为唯一标识。AudioDeviceDescriptors的具体信息可以参考[AudioDeviceDescriptors](../reference/apis/js-apis-audio.md#audiodevicedescriptors)
> **说明:**
>
> 用户可以选择连接一组音频设备(如一对蓝牙耳机),但系统侧只感知为一个设备,该组设备共用一个设备id。
```ts
let inputAudioDeviceDescriptor = [{
deviceRole : audio.DeviceRole.INPUT_DEVICE,
deviceType : audio.DeviceType.EARPIECE,
id : 1,
name : "",
address : "",
sampleRates : [44100],
channelCounts : [2],
channelMasks : [0],
networkId : audio.LOCAL_NETWORK_ID,
interruptGroupId : 1,
volumeGroupId : 1,
}];
async function getRoutingManager(){
audioRoutingManager.selectInputDevice(inputAudioDeviceDescriptor).then(() => {
console.info('Invoke selectInputDevice succeeded.');
}).catch((err) => {
console.error(`Invoke selectInputDevice failed, code is ${err.code}, message is ${err.message}`);
});
}
```
# 音频焦点模式开发指导
## 简介
音频焦点模式指的是应用内,允许对多个声音的播放进行控制。<br>
音频应用可以在AudioRenderer下设置独立焦点模式、共享焦点模式。<br>
当设置在共享的模式下,多个音频共用一个会话ID;独立焦点模式下,每一个音频拥有单独会话ID。
- **异步操作**:为保证UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用Promise函数。
## 开发指导
详细API含义可参考:[音频管理API文档AudioRenderer](../reference/apis/js-apis-audio.md#audiorenderer8)
1. 使用createAudioRenderer()创建一个AudioRenderer实例。<br>
在audioRendererOptions中设置相关参数。<br>
该实例可用于音频渲染、控制和获取采集状态,以及注册通知回调。<br>
```js
import audio from '@ohos.multimedia.audio';
var audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
}
var audioRendererInfo = {
content: audio.ContentType.CONTENT_TYPE_SPEECH,
usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
rendererFlags: 1
}
var audioRendererOptions = {
streamInfo: audioStreamInfo,
rendererInfo: audioRendererInfo
}
let audioRenderer = await audio.createAudioRenderer(audioRendererOptions);
```
2. 设置焦点模式。
在AudioRenderer初始化完毕后,可以进行焦点模式的设置。<br>
```js
var mode_ = audio.InterruptMode.SHARE_MODE;
await this.audioRenderer.setInterruptMode(mode_).then(() => {
console.log('[JSAR] [SetInterruptMode] 设置: ' + (mode_ == 0 ? "共享模式":"独立焦点模式") + "成功" );
});
```
# 音频输出设备管理
有时设备同时连接多个音频输出设备,需要指定音频输出设备进行音频播放,此时需要使用AudioRoutingManager接口进行输出设备的管理,API说明可以参考[AudioRoutingManager API文档](../reference/apis/js-apis-audio.md#audiomanager)
## 创建AudioRoutingManager实例
在使用AudioRoutingManager管理音频设备前,需要先导入模块并创建实例。
```ts
import audio from '@ohos.multimedia.audio'; // 导入audio模块
let audioManager = audio.getAudioManager(); // 需要先创建AudioManager实例
let audioRoutingManager = audioManager.getRoutingManager(); // 再调用AudioManager的方法创建AudioRoutingManager实例
```
## 支持的音频输出设备类型
目前支持的音频输出设备见下表:
| 名称 | 值 | 说明 |
| -------- | -------- | -------- |
| EARPIECE | 1 | 听筒。 |
| SPEAKER | 2 | 扬声器。 |
| WIRED_HEADSET | 3 | 有线耳机,带麦克风。 |
| WIRED_HEADPHONES | 4 | 有线耳机,无麦克风。 |
| BLUETOOTH_SCO | 7 | 蓝牙设备SCO(Synchronous&nbsp;Connection&nbsp;Oriented)连接。 |
| BLUETOOTH_A2DP | 8 | 蓝牙设备A2DP(Advanced&nbsp;Audio&nbsp;Distribution&nbsp;Profile)连接。 |
| USB_HEADSET | 22 | USB耳机,带麦克风。 |
## 获取输出设备信息
使用getDevices()方法可以获取当前所有输出设备的信息。
```ts
audioRoutingManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG).then((data) => {
console.info('Promise returned to indicate that the device list is obtained.');
});
```
## 监听设备连接状态变化
可以设置监听事件来监听设备连接状态的变化,当有设备连接或断开时触发回调:
```ts
// 监听音频设备状态变化
audioRoutingManager.on('deviceChange', audio.DeviceFlag.OUTPUT_DEVICES_FLAG, (deviceChanged) => {
console.info('device change type : ' + deviceChanged.type); // 设备连接状态变化,0为连接,1为断开连接
console.info('device descriptor size : ' + deviceChanged.deviceDescriptors.length);
console.info('device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceRole); // 设备角色
console.info('device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceType); // 设备类型
});
// 取消监听音频设备状态变化
audioRoutingManager.off('deviceChange', (deviceChanged) => {
console.info('Should be no callback.');
});
```
## 选择音频输出设备(仅对系统应用开放)
选择音频输出设备,当前只能选择一个输出设备,以设备ID作为唯一标识。AudioDeviceDescriptors的具体信息可以参考[AudioDeviceDescriptors](../reference/apis/js-apis-audio.md#audiodevicedescriptors)
> **说明:**
>
> 用户可以选择连接一组音频设备(如一对蓝牙耳机),但系统侧只感知为一个设备,该组设备共用一个设备ID。
```ts
let outputAudioDeviceDescriptor = [{
deviceRole : audio.DeviceRole.OUTPUT_DEVICE,
deviceType : audio.DeviceType.SPEAKER,
id : 1,
name : "",
address : "",
sampleRates : [44100],
channelCounts : [2],
channelMasks : [0],
networkId : audio.LOCAL_NETWORK_ID,
interruptGroupId : 1,
volumeGroupId : 1,
}];
async function selectOutputDevice(){
audioRoutingManager.selectOutputDevice(outputAudioDeviceDescriptor).then(() => {
console.info('Invoke selectOutputDevice succeeded.');
}).catch((err) => {
console.error(`Invoke selectOutputDevice failed, code is ${err.code}, message is ${err.message}`);
});
}
```
# 音频开发概述
音频模块支持音频业务的开发,提供音频相关的功能,主要包括音频播放、音量管理等。
## 基本概念
- **采样**<br/>
采样是指将连续时域上的模拟信号按照一定的时间间隔采样,获取到离散时域上离散信号的过程。
- **采样率**<br/>
采样率为每秒从连续信号中提取并组成离散信号的采样次数,单位用赫兹(Hz)来表示。通常人耳能听到频率范围大约在20Hz~20kHz之间的声音。常用的音频采样频率有:8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz、96kHz、192kHz等。
- **声道**<br/>
声道是指声音在录制或播放时在不同空间位置采集或回放的相互独立的音频信号,所以声道数也就是声音录制时的音源数量或回放时相应的扬声器数量。
- **音频帧**<br/>
音频数据是流式的,本身没有明确的一帧帧的概念,在实际的应用中,为了音频算法处理/传输的方便,一般约定俗成取2.5ms~60ms为单位的数据量为一帧音频。这个时间被称之为“采样时间”,其长度没有特别的标准,它是根据编解码器和具体应用的需求来决定的。
- **PCM**<br/>
PCM(Pulse Code Modulation),即脉冲编码调制,是一种将模拟信号数字化的方法,是将时间连续、取值连续的模拟信号转换成时间离散、抽样值离散的数字信号的过程。
# 多音频播放的并发策略
## 音频打断策略
多音频并发,即多个音频流同时播放。此场景下,如果系统不加管控,会造成多个音频流混音播放,容易让用户感到嘈杂,造成不好的用户体验。为了解决这个问题,系统预设了音频打断策略,对多音频播放的并发进行管控,只有持有音频焦点的音频流才可以正常播放,避免多个音频流无序并发播放的现象出现。
当应用开始播放音频时,系统首先为相应的音频流申请音频焦点,获得焦点的音频流可以播放;若焦点申请被拒绝,则不能播放。在音频流播放的过程中,若被其他音频流打断,则会失去音频焦点。当音频流失去音频焦点时,只能暂停播放。在应用播放音频的过程中,这些动作均由系统自行完成,无需应用主动触发。但为了维持应用和系统的状态一致性,保证良好的用户体验,推荐应用[监听音频打断事件](#监听音频打断事件),并在收到音频打断事件([InterruptEvent](../reference/apis/js-apis-audio.md#interruptevent9))时做出相应处理。
为满足应用对多音频并发策略的不同需求,音频打断策略预设了两种焦点模式,针对同一应用创建的多个音频流,应用可通过设置[焦点模式](#焦点模式),选择由应用自主管控或由系统统一管控。
音频打断策略决定了应该对音频流采取何种操作,如暂停播放、继续播放、降低音量播放、恢复音量播放等,这些操作可能由系统或应用来执行。音频打断策略预置了两种[打断类型](#打断类型),用于区分音频打断事件(InterruptEvent)的执行者。
### 焦点模式
音频打断策略预设了两种焦点模式([InterruptMode](../reference/apis/js-apis-audio.md#interruptmode9)):
- 共享焦点模式(SHARED_MODE):由同一应用创建的多个音频流,共享一个音频焦点。这些音频流之间的并发规则由应用自主决定,音频打断策略不会介入。当其他应用创建的音频流与该应用的音频流并发播放时,才会触发音频打断策略的管控。
- 独立焦点模式(INDEPENDENT_MODE):应用创建的每一个音频流均会独立拥有一个音频焦点,当多个音频流并发播放时,会触发音频打断策略的管控。
应用可以按需选择合适的焦点模式,在创建音频流时,系统默认采用共享焦点模式,应用可主动设置所需的模式。
设置焦点模式的方法:
-[使用AVPlayer开发音频播放功能](using-avplayer-for-playback.md),则可以通过修改AVPlayer的[audioInterruptMode](../reference/apis/js-apis-media.md#avplayer9)属性进行设置。
-[使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md),则可以调用AudioRenderer的[setInterruptMode](../reference/apis/js-apis-audio.md#setinterruptmode9)函数进行设置。
### 打断类型
音频打断策略(包括两种焦点模式)决定了应该对各个音频流采取何种操作,如暂停播放、继续播放、降低音量播放、恢复音量播放等。而针对这些操作的执行过程,根据执行者的不同,可以分为两种打断类型([InterruptForceType](../reference/apis/js-apis-audio.md#interruptforcetype9)):
- 强制打断类型(INTERRUPT_FORCE):由系统进行操作,强制打断音频播放。
- 共享打断类型(INTERRUPT_SHARE):由应用进行操作,可以选择打断或忽略。
对于音频打断策略的执行,系统默认采用强制打断类型(INTERRUPT_FORCE),应用无法更改。但对于一些策略(如继续播放等),系统无法强制执行,所以这两种打断类型均可能出现。应用可根据音频打断事件(InterruptEvent)的成员变量forceType的值,获取该事件采用的打断类型。
在应用播放音频的过程中,系统自动为音频流执行申请焦点、持有焦点、释放焦点等动作,当发生音频打断事件时,系统强制对音频流执行暂停、停止、降低音量、恢复音量等操作,并向应用发送音频打断事件(InterruptEvent)回调。由于系统会强制改变音频流状态,为了维持应用和系统的状态一致性,保证良好的用户体验,推荐应用[监听音频打断事件](#监听音频打断事件),并在收到音频打断事件(InterruptEvent)时做出相应处理。
对于一些系统无法强制执行的操作(例如音频流继续播放的场景),会向应用发送包含了共享打断类型的音频打断事件,由应用自行执行相应操作,此时应用可以选择执行或忽略,系统不会干涉。
## 监听音频打断事件
在应用播放音频时,推荐应用监听音频打断事件,当音频打断事件发生时,系统会根据预设策略,对音频流做出相应的操作,并针对状态发生改变的音频流,向所属的应用发送音频打断事件。
应用收到音频打断事件后,需根据其内容提示,做出相应的处理,避免出现应用状态与预期效果不一致的问题。
监听音频打断事件的方法:
-[使用AVPlayer开发音频播放功能](using-avplayer-for-playback.md),则可以调用AVPlayer的[on('audioInterrupt')](../reference/apis/js-apis-media.md#onaudiointerrupt9)函数进行监听,当收到音频打断事件(InterruptEvent)时,应用需根据其内容,做出相应的调整。
-[使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md),则可以调用AudioRenderer的[on('audioInterrupt')](../reference/apis/js-apis-audio.md#onaudiointerrupt9)函数进行监听,当收到音频打断事件(InterruptEvent)时,应用需根据其内容,做出相应的调整。
为了带给用户更好的体验,针对不同的音频打断事件内容,应用需要做出相应的处理操作。此处以使用AudioRenderer开发音频播放功能为例,展示推荐应用采取的处理方法,提供伪代码供开发者参考(若使用AVPlayer开发音频播放功能,处理方法类似),具体的代码实现,开发者可结合实际情况编写,处理方法也可自行调整。
```ts
let isPlay; // 是否正在播放,实际开发中,对应与音频播放状态相关的模块
let isDucked; //是否降低音量,实际开发中,对应与音频音量相关的模块
let started; // 标识符,记录“开始播放(start)”操作是否成功
async function onAudioInterrupt(){
// 此处以使用AudioRenderer开发音频播放功能举例,变量audioRenderer即为播放时创建的AudioRenderer实例。
audioRenderer.on('audioInterrupt', async(interruptEvent) => {
// 在发生音频打断事件时,audioRenderer收到interruptEvent回调,此处根据其内容做相应处理
// 先读取interruptEvent.forceType的类型,判断系统是否已强制执行相应操作
// 再读取interruptEvent.hintType的类型,做出相应的处理
if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_FORCE) {
// 强制打断类型(INTERRUPT_FORCE):音频相关处理已由系统执行,应用需更新自身状态,做相应调整
switch (interruptEvent.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
// 此分支表示系统已将音频流暂停(临时失去焦点),为保持状态一致,应用需切换至音频暂停状态
// 临时失去焦点:待其他音频流释放音频焦点后,本音频流会收到resume对应的音频打断事件,到时可自行继续播放
isPlay = false; // 此句为简化处理,代表应用切换至音频暂停状态的若干操作
break;
case audio.InterruptHint.INTERRUPT_HINT_STOP:
// 此分支表示系统已将音频流停止(永久失去焦点),为保持状态一致,应用需切换至音频暂停状态
// 永久失去焦点:后续不会再收到任何音频打断事件,若想恢复播放,需要用户主动触发。
isPlay = false; // 此句为简化处理,代表应用切换至音频暂停状态的若干操作
break;
case audio.InterruptHint.INTERRUPT_HINT_DUCK:
// 此分支表示系统已将音频音量降低(默认降到正常音量的20%),为保持状态一致,应用需切换至降低音量播放状态
// 若应用不接受降低音量播放,可在此处选择其他处理方式,如主动暂停等
isDucked = true; // 此句为简化处理,代表应用切换至降低音量播放状态的若干操作
break;
case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
// 此分支表示系统已将音频音量恢复正常,为保持状态一致,应用需切换至正常音量播放状态
isDucked = false; // 此句为简化处理,代表应用切换至正常音量播放状态的若干操作
break;
default:
break;
}
} else if (interruptEvent.forceType === audio.InterruptForceType.INTERRUPT_SHARE) {
// 共享打断类型(INTERRUPT_SHARE):应用可自主选择执行相关操作或忽略音频打断事件
switch (interruptEvent.hintType) {
case audio.InterruptHint.INTERRUPT_HINT_RESUME:
// 此分支表示临时失去焦点后被暂停的音频流此时可以继续播放,建议应用继续播放,切换至音频播放状态
// 若应用此时不想继续播放,可以忽略此音频打断事件,不进行处理即可
// 继续播放,此处主动执行start(),以标识符变量started记录start()的执行结果
await audioRenderer.start().then(async function () {
started = true; // start()执行成功
}).catch((err) => {
started = false; // start()执行失败
});
// 若start()执行成功,则切换至音频播放状态
if (started) {
isPlay = true; // 此句为简化处理,代表应用切换至音频播放状态的若干操作
} else {
// 音频继续播放执行失败
}
break;
default:
break;
}
}
});
}
```
# 音频播放开发概述
## 如何选择音频播放开发方式
在OpenHarmony系统中,多种API都提供了音频播放开发的支持,不同的API适用于不同音频数据格式、音频资源来源、音频使用场景,甚至是不同开发语言。因此,选择合适的音频播放API,有助于降低开发工作量,实现更佳的音频播放效果。
- [AVPlayer](using-avplayer-for-playback.md):功能较完善的音频、视频播放ArkTS/JS API,集成了流媒体和本地资源解析、媒体资源解封装、音频解码和音频输出功能。可以用于直接播放mp3、m4a等格式的音频文件,不支持直接播放PCM格式文件。
- [AudioRenderer](using-audiorenderer-for-playback.md):用于音频输出的的ArkTS/JS API,仅支持PCM格式,需要应用需要持续写入音频数据进行工作。应用可以在输入前添加数据预处理,如设定音频文件的采样率、位宽等,要求开发者具备音频处理的基础知识,适用于更专业、更多样化的媒体播放应用开发。
- [OpenSLES](using-opensl-es-for-playback.md):一套跨平台标准化的音频Native API,目前阶段唯一的音频类Native API,同样提供音频输出能力,仅支持PCM格式,适用于从其他嵌入式平台移植,或依赖在Native层实现音频输出功能的播放应用使用。
- [TonePlayer](using-toneplayer-for-playback.md):拨号和回铃音播放ArkTS/JS API,只能在固定的类型范围内选择播放内容,无需输入媒体资源或音频数据,适用于拨号盘按键和通话回铃音的特定场景。该功能当前仅对系统应用开放。
- 在音频播放中,应用时常需要用到一些急促简短的音效,如相机快门音效、按键音效、游戏射击音效等,当前只能使用AVPlayer播放音频文件替代实现,在OpenHarmony后续版本将会推出相关接口来支持该场景。
## 开发音频播放应用须知
应用如果要实现后台播放或熄屏播放,需要同时满足:
1. 使用媒体会话功能注册到系统内统一管理,否则在应用进入后台时,播放将被强制停止。具体参考[媒体会话开发指导](avsession-overview.md)
2. 申请长时任务避免进入挂起(Suspend)状态。具体参考[长时任务开发指导](../task-management/continuous-task-dev-guide.md)
当应用进入后台,播放被中断,如果被媒体会话管控,将打印日志“pause id”;如果没有该日志,则说明被长时任务管控。
# 音频播放流管理
对于播放音频类的应用,开发者需要关注该应用的音频流的状态以做出相应的操作,比如监听到状态为播放中/暂停时,及时改变播放按钮的UI显示。
## 读取或监听应用内音频流状态变化
参考[使用AudioRenderer开发音频播放功能](using-audiorenderer-for-playback.md)[audio.createAudioRenderer](../reference/apis/js-apis-audio.md#audiocreateaudiorenderer8),完成AudioRenderer的创建,然后可以通过以下两种方式查看音频流状态的变化:
- 方法1:直接查看AudioRenderer的[state](../reference/apis/js-apis-audio.md#属性)
```ts
let audioRendererState = audioRenderer.state;
console.info(`Current state is: ${audioRendererState }`)
```
- 方法2:注册stateChange监听AudioRenderer的状态变化:
```ts
audioRenderer.on('stateChange', (rendererState) => {
console.info(`State change to: ${rendererState}`)
});
```
获取state后可对照[AudioState](../reference/apis/js-apis-audio.md#audiostate8)来进行相应的操作,比如更改暂停播放按钮的显示等。
## 读取或监听所有音频流的变化
如果部分应用需要查询获取所有音频流的变化信息,可以通过AudioStreamManager读取或监听所有音频流的变化。
> **说明:**
>
> 对于标记为系统接口(system api)的音频流变化信息需要系统级别应用才可查看,若应用不是系统应用,将无法获取准确信息。
如下为音频流管理调用关系图:
![Audio stream management invoking relationship](figures/audio-stream-mgmt-invoking-relationship.png)
在进行应用开发的过程中,开发者需要使用getStreamManager()创建一个AudioStreamManager实例,进而通过该实例管理音频流。开发者可通过调用on('audioRendererChange')监听音频流的变化,在音频流状态变化、设备变化时获得通知。同时可通过off('audioRendererChange')取消相关事件的监听。另外,开发者可以主动调用getCurrentAudioRendererInfoArray()来查询播放流的唯一ID、播放流客户端的UID、音频流状态等信息。
详细API含义可参考[音频管理API文档AudioStreamManager](../reference/apis/js-apis-audio.md#audiostreammanager9)
## 开发步骤及注意事项
1. 创建AudioStreamManager实例。
在使用AudioStreamManager的API前,需要使用getStreamManager()创建一个AudioStreamManager实例。
```ts
import audio from '@ohos.multimedia.audio';
let audioManager = audio.getAudioManager();
let audioStreamManager = audioManager.getStreamManager();
```
2. 使用on('audioRendererChange')监听音频播放流的变化。 如果音频流监听应用需要在音频播放流状态变化、设备变化时获取通知,可以订阅该事件。
```ts
audioStreamManager.on('audioRendererChange', (AudioRendererChangeInfoArray) => {
for (let i = 0; i < AudioRendererChangeInfoArray.length; i++) {
let AudioRendererChangeInfo = AudioRendererChangeInfoArray[i];
console.info(`## RendererChange on is called for ${i} ##`);
console.info(`StreamId for ${i} is: ${AudioRendererChangeInfo.streamId}`);
console.info(`Content ${i} is: ${AudioRendererChangeInfo.rendererInfo.content}`);
console.info(`Stream ${i} is: ${AudioRendererChangeInfo.rendererInfo.usage}`);
console.info(`Flag ${i} is: ${AudioRendererChangeInfo.rendererInfo.rendererFlags}`);
for (let j = 0;j < AudioRendererChangeInfo.deviceDescriptors.length; j++) {
console.info(`Id: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].id}`);
console.info(`Type: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].deviceType}`);
console.info(`Role: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].deviceRole}`);
console.info(`Name: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].name}`);
console.info(`Address: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].address}`);
console.info(`SampleRates: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].sampleRates[0]}`);
console.info(`ChannelCount ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].channelCounts[0]}`);
console.info(`ChannelMask: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].channelMasks}`);
}
}
});
```
3. (可选)使用off('audioRendererChange')取消监听音频播放流变化。
```ts
audioStreamManager.off('audioRendererChange');
console.info('RendererChange Off is called ');
```
4. (可选)使用getCurrentAudioRendererInfoArray()获取所有音频播放流的信息。
该接口可获取音频播放流唯一ID,音频播放客户端的UID,音频状态以及音频播放器的其他信息。
> **说明:**
>
> 对所有音频流状态进行监听的应用需要[申请权限](../security/accesstoken-guidelines.md)ohos.permission.USE_BLUETOOTH,否则无法获得实际的设备名称和设备地址信息,查询到的设备名称和设备地址(蓝牙设备的相关属性)将为空字符串。
```ts
async function getCurrentAudioRendererInfoArray(){
await audioStreamManager.getCurrentAudioRendererInfoArray().then( function (AudioRendererChangeInfoArray) {
console.info(`getCurrentAudioRendererInfoArray Get Promise is called `);
if (AudioRendererChangeInfoArray != null) {
for (let i = 0; i < AudioRendererChangeInfoArray.length; i++) {
let AudioRendererChangeInfo = AudioRendererChangeInfoArray[i];
console.info(`StreamId for ${i} is: ${AudioRendererChangeInfo.streamId}`);
console.info(`Content ${i} is: ${AudioRendererChangeInfo.rendererInfo.content}`);
console.info(`Stream ${i} is: ${AudioRendererChangeInfo.rendererInfo.usage}`);
console.info(`Flag ${i} is: ${AudioRendererChangeInfo.rendererInfo.rendererFlags}`);
for (let j = 0;j < AudioRendererChangeInfo.deviceDescriptors.length; j++) {
console.info(`Id: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].id}`);
console.info(`Type: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].deviceType}`);
console.info(`Role: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].deviceRole}`);
console.info(`Name: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].name}`);
console.info(`Address: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].address}`);
console.info(`SampleRates: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].sampleRates[0]}`);
console.info(`ChannelCount ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].channelCounts[0]}`);
console.info(`ChannelMask: ${i} : ${AudioRendererChangeInfo.deviceDescriptors[j].channelMasks}`);
}
}
}
}).catch((err) => {
console.error(`Invoke getCurrentAudioRendererInfoArray failed, code is ${err.code}, message is ${err.message}`);
});
}
```
# 音频播放开发指导
## 简介
音频播放的主要工作是将音频数据转码为可听见的音频模拟信号,并通过输出设备进行播放,同时对播放任务进行管理,包括开始播放、暂停播放、停止播放、释放资源、设置音量、跳转播放位置、获取轨道信息等功能控制。
## 运作机制
该模块提供了音频播放状态变化示意图和音频播放外部模块交互图。
**图1** 音频播放状态变化示意图
![zh-ch_image_audio_state_machine](figures/zh-ch_image_audio_state_machine.png)
**注意**:当前为Idle状态,设置src不会改变状态;且src设置成功后,不能再次设置其它src,需调用reset()接口后,才能重新设置src。
**图2** 音频播放外部模块交互图
![zh-ch_image_audio_player](figures/zh-ch_image_audio_player.png)
**说明**:三方应用通过调用JS接口层提供的js接口实现相应功能时,框架层会通过Native Framework的媒体服务,调用音频部件,将软件解码后的音频数据输出至硬件接口层的音频HDI,实现音频播放功能。
## 开发指导
详细API含义可参考:[媒体服务API文档AudioPlayer](../reference/apis/js-apis-media.md#audioplayer)
> **说明:**
>
> path路径在FA模型和Stage模型下的获取方式不同,示例代码中仅给出pathDir示例,具体的path路径请开发者根据实际情况获取。获取方式请参考[应用沙箱路径使用说明](../reference/apis/js-apis-fileio.md#使用说明)。
### 全流程场景
音频播放的全流程场景包含:创建实例,设置uri,播放音频,跳转播放位置,设置音量,暂停播放,获取轨道信息,停止播放,重置,释放资源等流程。
AudioPlayer支持的src媒体源输入类型可参考:[src属性说明](../reference/apis/js-apis-media.md#audioplayer_属性)
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
// 打印码流轨道信息
function printfDescription(obj) {
for (let item in obj) {
let property = obj[item];
console.info('audio key is ' + item);
console.info('audio value is ' + property);
}
}
// 设置播放器回调函数
function setCallBack(audioPlayer) {
audioPlayer.on('dataLoad', () => { // 设置'dataLoad'事件回调,src属性设置成功后,触发此回调
console.info('audio set source success');
audioPlayer.play(); // 需等待'dataLoad'事件回调完成后,才可调用play进行播放,触发'play'事件回调
});
audioPlayer.on('play', () => { // 设置'play'事件回调
console.info('audio play success');
audioPlayer.pause(); // 触发'pause'事件回调,暂停播放
});
audioPlayer.on('pause', () => { // 设置'pause'事件回调
console.info('audio pause success');
audioPlayer.seek(5000); // 触发'timeUpdate'事件回调,seek到5000ms处播放
});
audioPlayer.on('stop', () => { // 设置'stop'事件回调
console.info('audio stop success');
audioPlayer.reset(); // 触发'reset'事件回调后,重新设置src属性,可完成切歌
});
audioPlayer.on('reset', () => { // 设置'reset'事件回调
console.info('audio reset success');
audioPlayer.release(); // audioPlayer资源被销毁
audioPlayer = undefined;
});
audioPlayer.on('timeUpdate', (seekDoneTime) => { // 设置'timeUpdate'事件回调
if (typeof(seekDoneTime) == 'undefined') {
console.info('audio seek fail');
return;
}
console.info('audio seek success, and seek time is ' + seekDoneTime);
audioPlayer.setVolume(0.5); // 触发'volumeChange'事件回调
});
audioPlayer.on('volumeChange', () => { // 设置'volumeChange'事件回调
console.info('audio volumeChange success');
audioPlayer.getTrackDescription((error, arrlist) => { // 通过回调方式获取音频轨道信息
if (typeof (arrlist) != 'undefined') {
for (let i = 0; i < arrlist.length; i++) {
printfDescription(arrlist[i]);
}
} else {
console.log(`audio getTrackDescription fail, error:${error.message}`);
}
audioPlayer.stop(); // 触发'stop'事件回调,停止播放
});
});
audioPlayer.on('finish', () => { // 设置'finish'事件回调,播放完成触发
console.info('audio play finish');
});
audioPlayer.on('error', (error) => { // 设置'error'事件回调
console.info(`audio error called, errName is ${error.name}`);
console.info(`audio error called, errCode is ${error.code}`);
console.info(`audio error called, errMessage is ${error.message}`);
});
}
async function audioPlayerDemo() {
// 1. 创建实例
let audioPlayer = media.createAudioPlayer();
setCallBack(audioPlayer); // 设置事件回调
// 2. 用户选择音频,设置uri
let fdPath = 'fd://'
let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。
// path路径的码流可通过"hdc file send D:\xxx\01.mp3 /data/app/el2/100/base/ohos.acts.multimedia.audio.audioplayer/haps/entry/files" 命令,将其推送到设备上
let path = pathDir + '/01.mp3'
let file = await fs.open(path);
fdPath = fdPath + '' + file.fd;
audioPlayer.src = fdPath; // 设置src属性,并触发'dataLoad'事件回调
}
```
### 正常播放场景
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
export class AudioDemo {
// 设置播放器回调函数
setCallBack(audioPlayer) {
audioPlayer.on('dataLoad', () => { // 设置'dataLoad'事件回调,src属性设置成功后,触发此回调
console.info('audio set source success');
audioPlayer.play(); // 调用play方法开始播放,触发'play'事件回调
});
audioPlayer.on('play', () => { // 设置'play'事件回调
console.info('audio play success');
});
audioPlayer.on('finish', () => { // 设置'finish'事件回调,播放完成触发
console.info('audio play finish');
audioPlayer.release(); // audioPlayer资源被销毁
audioPlayer = undefined;
});
}
async audioPlayerDemo() {
let audioPlayer = media.createAudioPlayer(); // 创建一个音频播放实例
this.setCallBack(audioPlayer); // 设置事件回调
let fdPath = 'fd://'
let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。
// path路径的码流可通过"hdc file send D:\xxx\01.mp3 /data/app/el2/100/base/ohos.acts.multimedia.audio.audioplayer/haps/entry/files" 命令,将其推送到设备上
let path = pathDir + '/01.mp3'
let file = await fs.open(path);
fdPath = fdPath + '' + file.fd;
audioPlayer.src = fdPath; // 设置src属性,并触发'dataLoad'事件回调
}
}
```
### 切歌场景
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
export class AudioDemo {
// 设置播放器回调函数
private isNextMusic = false;
setCallBack(audioPlayer) {
audioPlayer.on('dataLoad', () => { // 设置'dataLoad'事件回调,src属性设置成功后,触发此回调
console.info('audio set source success');
audioPlayer.play(); // 调用play方法开始播放,触发'play'事件回调
});
audioPlayer.on('play', () => { // 设置'play'事件回调
console.info('audio play success');
audioPlayer.reset(); // 调用reset方法,触发'reset'事件回调
});
audioPlayer.on('reset', () => { // 设置'reset'事件回调
console.info('audio play success');
if (!this.isNextMusic) { // 当isNextMusic 为false时,实现切歌功能
this.nextMusic(audioPlayer); // 实现切歌功能
} else {
audioPlayer.release(); // audioPlayer资源被销毁
audioPlayer = undefined;
}
});
}
async nextMusic(audioPlayer) {
this.isNextMusic = true;
let nextFdPath = 'fd://'
let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。
// path路径的码流可通过"hdc file send D:\xxx\02.mp3 /data/app/el2/100/base/ohos.acts.multimedia.audio.audioplayer/haps/entry/files" 命令,将其推送到设备上
let nextpath = pathDir + '/02.mp3'
let nextFile = await fs.open(nextpath);
nextFdPath = nextFdPath + '' + nextFile.fd;
audioPlayer.src = nextFdPath; // 设置src属性,并重新触发触发'dataLoad'事件回调
}
async audioPlayerDemo() {
let audioPlayer = media.createAudioPlayer(); // 创建一个音频播放实例
this.setCallBack(audioPlayer); // 设置事件回调
let fdPath = 'fd://'
let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。
// path路径的码流可通过"hdc file send D:\xxx\01.mp3 /data/app/el2/100/base/ohos.acts.multimedia.audio.audioplayer/haps/entry/files" 命令,将其推送到设备上
let path = pathDir + '/01.mp3'
let file = await fs.open(path);
fdPath = fdPath + '' + file.fd;
audioPlayer.src = fdPath; // 设置src属性,并触发'dataLoad'事件回调
}
}
```
### 单曲循环场景
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
export class AudioDemo {
// 设置播放器回调函数
setCallBack(audioPlayer) {
audioPlayer.on('dataLoad', () => { // 设置'dataLoad'事件回调,src属性设置成功后,触发此回调
console.info('audio set source success');
audioPlayer.loop = true; // 设置循环播放属性
audioPlayer.play(); // 调用play方法开始播放,触发'play'事件回调
});
audioPlayer.on('play', () => { // 设置'play'事件回调,开始循环播放
console.info('audio play success');
});
}
async audioPlayerDemo() {
let audioPlayer = media.createAudioPlayer(); // 创建一个音频播放实例
this.setCallBack(audioPlayer); // 设置事件回调
let fdPath = 'fd://'
let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。
// path路径的码流可通过"hdc file send D:\xxx\01.mp3 /data/app/el2/100/base/ohos.acts.multimedia.audio.audioplayer/haps/entry/files" 命令,将其推送到设备上
let path = pathDir + '/01.mp3'
let file = await fs.open(path);
fdPath = fdPath + '' + file.fd;
audioPlayer.src = fdPath; // 设置src属性,并触发'dataLoad'事件回调
}
}
```
## 相关实例
针对音频播放开发,有以下相关实例可供参考:
- [`JsDistributedMusicPlayer:`分布式音乐播放(JS)(API10)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/SuperFeature/DistributedAppDev/JsDistributedMusicPlayer)
- [`MediaCollections`:媒体管理合集(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/FileManagement/MediaCollections)
- [`Recorder`: 录音机(ArkTS)(API9)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Media/Recorder)
- [音频播放器(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/Media/Audio_OH_ETS)
\ No newline at end of file
# 音频录制开发指导
## 简介
音频录制的主要工作是捕获音频信号,完成音频编码并保存到文件中,帮助开发者轻松实现音频录制功能。该模块允许调用者指定音频录制的采样率、声道数、编码格式、封装格式、输出文件的路径等参数。
## 运作机制
该模块提供了音频录制状态变化示意图和音频录制外部模块交互图。
**图1** 音频录制状态变化变化示意图
![zh-ch_image_audio_recorder_state_machine](figures/zh-ch_image_audio_recorder_state_machine.png)
**图2** 音频录制外部模块交互图
![zh-ch_image_audio_recorder_zero](figures/zh-ch_image_audio_recorder_zero.png)
**说明**:三方录音应用或录音机通过调用JS接口层提供的js接口实现相应功能时,框架层会通过Native Framework的媒体服务,调用音频部件获取通过音频HDI捕获的音频数据,再通过软件编码输出编码封装后的音频数据保存至文件中,实现音频录制功能。
## 约束与限制
开发者在进行录制功能开发前,需要先对所开发的应用配置麦克风权限(ohos.permission.MICROPHONE),权限配置相关内容可参考:[访问控制权限申请指导](../security/accesstoken-guidelines.md)
## 开发指导
详细API含义可参考:[媒体服务API文档AudioRecorder](../reference/apis/js-apis-media.md#audiorecorder)
### 全流程场景
音频录制的全流程场景包含:创建实例,设置录制参数,开始录制,暂停录制,恢复录制,停止录制,释放资源等流程。
```js
import media from '@ohos.multimedia.media'
import mediaLibrary from '@ohos.multimedia.mediaLibrary'
export class AudioRecorderDemo {
private testFdNumber; // 用于保存fd地址
// 设置音频录制相关回调函数
setCallBack(audioRecorder) {
audioRecorder.on('prepare', () => { // 设置'prepare'事件回调
console.log('prepare success');
audioRecorder.start(); // 调用start方法开始录制,并触发start回调
});
audioRecorder.on('start', () => { // 设置'start'事件回调
console.log('audio recorder start success');
audioRecorder.pause(); // 调用pause方法暂停录制,并触发pause回调
});
audioRecorder.on('pause', () => { // 设置'pause'事件回调
console.log('audio recorder pause success');
audioRecorder.resume(); // 调用resume方法恢复录制,并触发resume回调
});
audioRecorder.on('resume', () => { // 设置'resume'事件回调
console.log('audio recorder resume success');
audioRecorder.stop(); // 调用stop方法停止录制,并触发stop回调
});
audioRecorder.on('stop', () => { // 设置'stop'事件回调
console.log('audio recorder stop success');
audioRecorder.reset(); // 调用reset方法重置录制,并触发reset回调
});
audioRecorder.on('reset', () => { // 设置'reset'事件回调
console.log('audio recorder reset success');
audioRecorder.release(); // 调用release方法,释放资源,并触发release回调
});
audioRecorder.on('release', () => { // 设置'release'事件回调
console.log('audio recorder release success');
audioRecorder = undefined;
});
audioRecorder.on('error', (error) => { // 设置'error'事件回调
console.info(`audio error called, errName is ${error.name}`);
console.info(`audio error called, errCode is ${error.code}`);
console.info(`audio error called, errMessage is ${error.message}`);
});
}
// pathName是传入的录制文件名,例如:01.mp3,生成后的文件地址:/storage/media/100/local/files/Video/01.mp3
// 使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA
async getFd(pathName) {
let displayName = pathName;
const mediaTest = mediaLibrary.getMediaLibrary();
let fileKeyObj = mediaLibrary.FileKey;
let mediaType = mediaLibrary.MediaType.VIDEO;
let publicPath = await mediaTest.getPublicDirectory(mediaLibrary.DirectoryType.DIR_VIDEO);
let dataUri = await mediaTest.createAsset(mediaType, displayName, publicPath);
if (dataUri != undefined) {
let args = dataUri.id.toString();
let fetchOp = {
selections : fileKeyObj.ID + "=?",
selectionArgs : [args],
}
let fetchFileResult = await mediaTest.getFileAssets(fetchOp);
let fileAsset = await fetchFileResult.getAllObject();
let fdNumber = await fileAsset[0].open('Rw');
this.testFdNumber = "fd://" + fdNumber.toString();
}
}
async audioRecorderDemo() {
// 1.创建实例
let audioRecorder = media.createAudioRecorder();
// 2.设置回调
this.setCallBack(audioRecorder);
await this.getFd('01.mp3'); // 调用getFd方法获取需要录制文件的fd地址
// 3.设置录制参数
let audioRecorderConfig = {
audioEncodeBitRate : 22050,
audioSampleRate : 22050,
numberOfChannels : 2,
uri : this.testFdNumber, // testFdNumber由getFd生成
location : { latitude : 30, longitude : 130},
audioEncoderMime : media.CodecMimeType.AUDIO_AAC,
fileFormat : media.ContainerFormatType.CFT_MPEG_4A,
}
audioRecorder.prepare(audioRecorderConfig); // 调用prepare方法,触发prepare回调函数
}
}
```
### 正常录制场景
与全流程场景不同,不包括暂停录制,恢复录制的过程。
```js
import media from '@ohos.multimedia.media'
import mediaLibrary from '@ohos.multimedia.mediaLibrary'
export class AudioRecorderDemo {
private testFdNumber; // 用于保存fd地址
// 设置音频录制相关回调函数
setCallBack(audioRecorder) {
audioRecorder.on('prepare', () => { // 设置'prepare'事件回调
console.log('prepare success');
audioRecorder.start(); // 调用start方法开始录制,并触发start回调
});
audioRecorder.on('start', () => { // 设置'start'事件回调
console.log('audio recorder start success');
audioRecorder.stop(); // 调用stop方法停止录制,并触发stop回调
});
audioRecorder.on('stop', () => { // 设置'stop'事件回调
console.log('audio recorder stop success');
audioRecorder.release(); // 调用release方法,释放资源,并触发release回调
});
audioRecorder.on('release', () => { // 设置'release'事件回调
console.log('audio recorder release success');
audioRecorder = undefined;
});
audioRecorder.on('error', (error) => { // 设置'error'事件回调
console.info(`audio error called, errName is ${error.name}`);
console.info(`audio error called, errCode is ${error.code}`);
console.info(`audio error called, errMessage is ${error.message}`);
});
}
// pathName是传入的录制文件名,例如:01.mp3,生成后的文件地址:/storage/media/100/local/files/Video/01.mp3
// 使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA
async getFd(pathName) {
let displayName = pathName;
const mediaTest = mediaLibrary.getMediaLibrary();
let fileKeyObj = mediaLibrary.FileKey;
let mediaType = mediaLibrary.MediaType.VIDEO;
let publicPath = await mediaTest.getPublicDirectory(mediaLibrary.DirectoryType.DIR_VIDEO);
let dataUri = await mediaTest.createAsset(mediaType, displayName, publicPath);
if (dataUri != undefined) {
let args = dataUri.id.toString();
let fetchOp = {
selections : fileKeyObj.ID + "=?",
selectionArgs : [args],
}
let fetchFileResult = await mediaTest.getFileAssets(fetchOp);
let fileAsset = await fetchFileResult.getAllObject();
let fdNumber = await fileAsset[0].open('Rw');
this.testFdNumber = "fd://" + fdNumber.toString();
}
}
async audioRecorderDemo() {
// 1.创建实例
let audioRecorder = media.createAudioRecorder();
// 2.设置回调
this.setCallBack(audioRecorder);
await this.getFd('01.mp3'); // 调用getFd方法获取需要录制文件的fd地址
// 3.设置录制参数
let audioRecorderConfig = {
audioEncodeBitRate : 22050,
audioSampleRate : 22050,
numberOfChannels : 2,
uri : this.testFdNumber, // testFdNumber由getFd生成
location : { latitude : 30, longitude : 130},
audioEncoderMime : media.CodecMimeType.AUDIO_AAC,
fileFormat : media.ContainerFormatType.CFT_MPEG_4A,
}
audioRecorder.prepare(audioRecorderConfig); // 调用prepare方法,触发prepare回调函数
}
}
```
## 相关实例
针对音频录制开发,有以下相关实例可供参考:
- [`Recorder:`录音机(ArkTS)(API9)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Media/Recorder)
- [音频播放器(ArkTS)(API9)](https://gitee.com/openharmony/codelabs/tree/master/Media/Audio_OH_ETS)
# 音频录制开发概述
## 如何选择音频录制开发方式
在OpenHarmony系统中,多种API都提供了音频录制开发的支持,不同的API适用于不同录音输出格式、音频使用场景或不同开发语言。因此,选择合适的音频录制API,有助于降低开发工作量,实现更佳的音频录制效果。
- [AVRecorder](using-avrecorder-for-recording.md):功能较完善的音频、视频录制ArkTS/JS API,集成了音频输入录制、音频编码和媒体封装的功能。开发者可以直接调用设备硬件如麦克风录音,并生成m4a音频文件。
- [AudioCapturer](using-audiocapturer-for-recording.md):用于音频输入的的ArkTS/JS API,仅支持PCM格式,需要应用持续读取音频数据进行工作。应用可以在音频输出后添加数据处理,要求开发者具备音频处理的基础知识,适用于更专业、更多样化的媒体播放应用开发。
- [OpenSLES](using-opensl-es-for-recording.md):一套跨平台标准化的音频Native API,目前阶段唯一的音频类Native API,同样提供音频输入原子能力,仅支持PCM格式,适用于从其他嵌入式平台移植,或依赖在Native层实现音频输入功能的录音应用使用。
## 开发音频录制应用须知
应用可以调用麦克风录制音频,但该行为属于隐私敏感行为,在调用麦克风前,需要先向用户申请权限“ohos.permission.MICROPHONE”。
权限申请的流程请参考[访问控制授权申请指导](../security/accesstoken-guidelines.md)。如何使用和管理麦克风请参考[管理麦克风](mic-management.md)
# 音频录制流管理
对于播放音频类的应用,开发者需要关注该应用的音频流的状态以做出相应的操作,比如监听到状态为结束时,及时提示用户录制已结束。
## 读取或监听应用内音频流状态变化
参考[使用AudioCapturer开发音频录制功能](using-audiocapturer-for-recording.md)[audio.createAudioCapturer](../apis/js-apis-audio.md#audiocreateaudiocapturer8),完成AudioRenderer的创建,然后可以通过以下两种方式查看音频流状态的变化:
- 方法1:直接查看AudioCapturer的[state](../apis/js-apis-audio.md#属性)
```ts
let audioCapturerState = audioCapturer.state;
console.info(`Current state is: ${audioCapturerState }`)
```
- 方法2:注册stateChange监听AudioCapturer的状态变化:
```ts
audioCapturer.on('stateChange', (capturerState) => {
console.info(`State change to: ${capturerState}`)
});
```
获取state后可对照[AudioState](../apis/js-apis-audio.md#audiostate8)来进行相应的操作,比如显示录制结束的提示等。
## 读取或监听所有录制流的变化
如果部分应用需要查询获取所有音频流的变化信息,可以通过AudioStreamManager读取或监听所有音频流的变化。
> **说明:**
>
> 对于标记为系统接口(system api)的音频流变化信息需要系统级别应用才可查看,若应用不是系统应用,将无法获取准确信息。
如下为音频流管理调用关系图:
![Invoking relationship of recording stream management](figures/invoking-relationship-recording-stream-mgmt.png)
在进行应用开发的过程中,开发者需要使用getStreamManager()创建一个AudioStreamManager实例,进而通过该实例管理音频流。开发者可通过调用on('audioCapturerChange')监听音频流的变化,在音频流状态变化、设备变化时获得通知,同时可通过off('audioCapturerChange')取消相关事件的监听。另外,开发者可以通过主动调用getCurrentAudioCapturerInfoArray()查询录制流的唯一ID、录制流客户端的UID、以及流状态等信息。
详细API含义可参考[音频管理API文档AudioStreamManager](../apis/js-apis-audio.md#audiostreammanager9)
## 开发步骤及注意事项
1. 创建AudioStreamManager实例。
在使用AudioStreamManager的API前,需要使用getStreamManager()创建一个AudioStreamManager实例。
```ts
import audio from '@ohos.multimedia.audio';
let audioManager = audio.getAudioManager();
let audioStreamManager = audioManager.getStreamManager();
```
2. 使用on('audioCapturerChange')监听音频录制流更改事件。 如果音频流监听应用需要在音频录制流状态变化、设备变化时获取通知,可以订阅该事件。
```ts
audioStreamManager.on('audioCapturerChange', (AudioCapturerChangeInfoArray) => {
for (let i = 0; i < AudioCapturerChangeInfoArray.length; i++) {
console.info(`## CapChange on is called for element ${i} ##`);
console.info(`StreamId for ${i} is: ${AudioCapturerChangeInfoArray[i].streamId}`);
console.info(`Source for ${i} is: ${AudioCapturerChangeInfoArray[i].capturerInfo.source}`);
console.info(`Flag ${i} is: ${AudioCapturerChangeInfoArray[i].capturerInfo.capturerFlags}`);
let devDescriptor = AudioCapturerChangeInfoArray[i].deviceDescriptors;
for (let j = 0; j < AudioCapturerChangeInfoArray[i].deviceDescriptors.length; j++) {
console.info(`Id: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].id}`);
console.info(`Type: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceType}`);
console.info(`Role: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceRole}`);
console.info(`Name: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].name}`);
console.info(`Address: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].address}`);
console.info(`SampleRates: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].sampleRates[0]}`);
console.info(`ChannelCounts ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelCounts[0]}`);
console.info(`ChannelMask: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelMasks}`);
}
}
});
```
3. (可选)使用off('audioCapturerChange')取消监听音频录制流变化。
```ts
audioStreamManager.off('audioCapturerChange');
console.info('CapturerChange Off is called');
```
4. (可选)使用getCurrentAudioCapturerInfoArray()获取当前音频录制流的信息。
该接口可获取音频录制流唯一ID,音频录制客户端的UID,音频状态以及音频捕获器的其他信息。
> **说明:**
> 对所有音频流状态进行监听的应用需要[申请权限](../security/accesstoken-guidelines.md)ohos.permission.USE_BLUETOOTH,否则无法获得实际的设备名称和设备地址信息,查询到的设备名称和设备地址(蓝牙设备的相关属性)将为空字符串。
```ts
async function getCurrentAudioCapturerInfoArray(){
await audioStreamManager.getCurrentAudioCapturerInfoArray().then( function (AudioCapturerChangeInfoArray) {
console.info('getCurrentAudioCapturerInfoArray Get Promise Called ');
if (AudioCapturerChangeInfoArray != null) {
for (let i = 0; i < AudioCapturerChangeInfoArray.length; i++) {
console.info(`StreamId for ${i} is: ${AudioCapturerChangeInfoArray[i].streamId}`);
console.info(`Source for ${i} is: ${AudioCapturerChangeInfoArray[i].capturerInfo.source}`);
console.info(`Flag ${i} is: ${AudioCapturerChangeInfoArray[i].capturerInfo.capturerFlags}`);
for (let j = 0; j < AudioCapturerChangeInfoArray[i].deviceDescriptors.length; j++) {
console.info(`Id: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].id}`);
console.info(`Type: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceType}`);
console.info(`Role: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceRole}`);
console.info(`Name: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].name}`);
console.info(`Address: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].address}`);
console.info(`SampleRates: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].sampleRates[0]}`);
console.info(`ChannelCounts ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelCounts[0]}`);
console.info(`ChannelMask: ${i} : ${AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelMasks}`);
}
}
}
}).catch((err) => {
console.error(`Invoke getCurrentAudioCapturerInfoArray failed, code is ${err.code}, message is ${err.message}`);
});
}
```
# 音频渲染开发指导
## 简介
AudioRenderer提供了渲染音频文件和控制播放的接口,开发者可以通过本指导,了解如何在输出设备中播放音频文件并管理播放任务。同时,AudioRenderer支持音频中断的功能。
开发者在调用AudioRenderer提供的各个接口时,需要理解以下名词:
- **音频中断**:当优先级较高的音频流需要播放时,AudioRenderer会中断优先级较低的流。例如,当用户在收听音乐时有来电,则优先级较低音乐播放将被暂停。
- **状态检查**:在进行应用开发的过程中,建议开发者通过on('stateChange')方法订阅AudioRenderer的状态变更。因为针对AudioRenderer的某些操作,仅在音频播放器在固定状态时才能执行。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
- **异步操作**:为保证UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用Promise函数,更多方式可参考[音频管理API文档AudioRenderer](../reference/apis/js-apis-audio.md#audiorenderer8)
- **焦点模式**:OpenHarmony中有两种焦点模式:**共享焦点模式****独立焦点模式**。其中,共享焦点模式是指,同一个应用创建的所有AudioRenderer对象共享一个焦点对象,应用内部无焦点转移,因此无法触发回调通知;独立焦点模式与之相反,即同一个应用创建的每个AudioRenderer对象都拥有独立的焦点对象,会发生焦点抢占,当应用内部发生焦点抢占,将会发生焦点转移,原本拥有焦点的AudioRenderer对象会获取到相关的回调通知。需要注意的是,默认情况下,应用创建的都是共享焦点,开发者可以调用setInterruptMode()来设置创建的焦点模式,完整示例请参考开发指导14。
## 运作机制
该模块提供了音频渲染模块的状态变化示意
**图1** 音频渲染状态示意图
![audio-renderer-state](figures/audio-renderer-state.png)
**PREPARED状态:** 通过调用create()方法进入到该状态。<br>
**RUNNING状态:** 正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在pause状态和stopped状态通过调用start()方法进入此状态。<br>
**PAUSED状态:** 在running状态可以通过pause()方法暂停音频数据的播放,暂停播放之后可以通过调用start()方法继续音频数据播放。<br>
**STOPPED状态:** 在paused状态可以通过调用stop()方法停止音频数据的播放,在running状态可以通过stop()方法停止音频数据的播放。<br>
**RELEASED状态:** 在prepared、paused、stop等状态,用户均可通过release()方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。<br>
## 开发指导
详细API含义可参考:[音频管理API文档AudioRenderer](../reference/apis/js-apis-audio.md#audiorenderer8)
1. 使用createAudioRenderer()创建一个全局的AudioRenderer实例,以便后续步骤使用。
在audioRendererOptions中设置相关参数。该实例可用于音频渲染、控制和获取渲染状态,以及注册通知回调。
```js
import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs';
//音频渲染相关接口自测试
@Entry
@Component
struct AudioRenderer1129 {
private audioRenderer: audio.AudioRenderer;
private bufferSize;//便于步骤3 write函数调用使用
private audioRenderer1: audio.AudioRenderer; //便于步骤14 完整示例调用使用
private audioRenderer2: audio.AudioRenderer; //便于步骤14 完整示例调用使用
async initAudioRender(){
let audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
}
let audioRendererInfo = {
content: audio.ContentType.CONTENT_TYPE_SPEECH,
usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0
}
let audioRendererOptions = {
streamInfo: audioStreamInfo,
rendererInfo: audioRendererInfo
}
this.audioRenderer = await audio.createAudioRenderer(audioRendererOptions);
console.log("Create audio renderer success.");
}
}
```
2. 调用start()方法来启动/恢复播放任务。
```js
async startRenderer() {
let state = this.audioRenderer.state;
// Renderer start时的状态应该是STATE_PREPARED、STATE_PAUSED和STATE_STOPPED之一.
if (state != audio.AudioState.STATE_PREPARED && state != audio.AudioState.STATE_PAUSED &&
state != audio.AudioState.STATE_STOPPED) {
console.info('Renderer is not in a correct state to start');
return;
}
await this.audioRenderer.start();
state = this.audioRenderer.state;
if (state == audio.AudioState.STATE_RUNNING) {
console.info('Renderer started');
} else {
console.error('Renderer start failed');
}
}
```
启动完成后,渲染器状态将变更为STATE_RUNNING,然后应用可以开始读取缓冲区。
3. 调用write()方法向缓冲区写入数据。
将需要播放的音频数据读入缓冲区,重复调用write()方法写入。请注意引入“import fs from '@ohos.file.fs';”,具体请参考步骤1。
```js
async writeData(){
// 此处是渲染器的合理的最小缓冲区大小(也可以选择其它大小的缓冲区)
this.bufferSize = await this.audioRenderer.getBufferSize();
let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径
const filePath = dir + '/file_example_WAV_2MG.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/file_example_WAV_2MG.wav
console.info(`file filePath: ${ filePath}`);
let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
let stat = await fs.stat(filePath); //音乐文件信息
let buf = new ArrayBuffer(this.bufferSize);
let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1);
for (let i = 0;i < len; i++) {
let options = {
offset: i * this.bufferSize,
length: this.bufferSize
}
let readsize = await fs.read(file.fd, buf, options)
let writeSize = await new Promise((resolve,reject)=>{
this.audioRenderer.write(buf,(err,writeSize)=>{
if(err){
reject(err)
}else{
resolve(writeSize)
}
})
})
}
fs.close(file)
await this.audioRenderer.stop(); //停止渲染
await this.audioRenderer.release(); //释放资源
}
```
4. (可选)调用pause()方法或stop()方法暂停/停止渲染音频数据。
```js
async pauseRenderer() {
let state = this.audioRenderer.state;
// 只有渲染器状态为STATE_RUNNING的时候才能暂停
if (state != audio.AudioState.STATE_RUNNING) {
console.info('Renderer is not running');
return;
}
await this.audioRenderer.pause();
state = this.audioRenderer.state;
if (state == audio.AudioState.STATE_PAUSED) {
console.info('Renderer paused');
} else {
console.error('Renderer pause failed');
}
}
async stopRenderer() {
let state = this.audioRenderer.state;
// 只有渲染器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止
if (state != audio.AudioState.STATE_RUNNING && state != audio.AudioState.STATE_PAUSED) {
console.info('Renderer is not running or paused');
return;
}
await this.audioRenderer.stop();
state = this.audioRenderer.state;
if (state == audio.AudioState.STATE_STOPPED) {
console.info('Renderer stopped');
} else {
console.error('Renderer stop failed');
}
}
```
5. (可选)调用drain()方法清空缓冲区。
```js
async drainRenderer() {
let state = this.audioRenderer.state;
// 只有渲染器状态为STATE_RUNNING的时候才能使用drain()
if (state != audio.AudioState.STATE_RUNNING) {
console.info('Renderer is not running');
return;
}
await this.audioRenderer.drain();
state = this.audioRenderer.state;
}
```
6. 任务完成,调用release()方法释放相关资源。
AudioRenderer会使用大量的系统资源,所以请确保完成相关任务后,进行资源释放。
```js
async releaseRenderer() {
let state = this.audioRenderer.state;
// 渲染器状态不是STATE_RELEASED或STATE_NEW状态,才能release
if (state == audio.AudioState.STATE_RELEASED || state == audio.AudioState.STATE_NEW) {
console.info('Renderer already released');
return;
}
await this.audioRenderer.release();
state = this.audioRenderer.state;
if (state == audio.AudioState.STATE_RELEASED) {
console.info('Renderer released');
} else {
console.info('Renderer release failed');
}
}
```
7. (可选)获取渲染器相关信息
通过以下代码,可以获取渲染器的相关信息。
```js
async getRenderInfo(){
// 获取当前渲染器状态
let state = this.audioRenderer.state;
// 获取渲染器信息
let audioRendererInfo : audio.AudioRendererInfo = await this.audioRenderer.getRendererInfo();
// 获取音频流信息
let audioStreamInfo : audio.AudioStreamInfo = await this.audioRenderer.getStreamInfo();
// 获取音频流ID
let audioStreamId : number = await this.audioRenderer.getAudioStreamId();
// 获取纳秒形式的Unix时间戳
let audioTime : number = await this.audioRenderer.getAudioTime();
// 获取合理的最小缓冲区大小
let bufferSize : number = await this.audioRenderer.getBufferSize();
// 获取渲染速率
let renderRate : audio.AudioRendererRate = await this.audioRenderer.getRenderRate();
}
```
8. (可选)设置渲染器相关信息
通过以下代码,可以设置渲染器的相关信息。
```js
async setAudioRenderInfo(){
// 设置渲染速率为正常速度
let renderRate : audio.AudioRendererRate = audio.AudioRendererRate.RENDER_RATE_NORMAL;
await this.audioRenderer.setRenderRate(renderRate);
// 设置渲染器音频中断模式为SHARE_MODE
let interruptMode : audio.InterruptMode = audio.InterruptMode.SHARE_MODE;
await this.audioRenderer.setInterruptMode(interruptMode);
// 设置一个流的音量为0.5
let volume : number = 0.5;
await this.audioRenderer.setVolume(volume);
}
```
9. (可选)使用on('audioInterrupt')方法订阅渲染器音频中断事件,使用off('audioInterrupt')取消订阅事件。
当优先级更高或相等的Stream-B请求激活并使用输出设备时,Stream-A被中断。
在某些情况下,框架会采取暂停播放、降低音量等强制操作,并通过InterruptEvent通知应用。在其他情况下,应用可以自行对InterruptEvent做出响应。
在音频中断的情况下,应用可能会碰到音频数据写入失败的问题。所以建议不感知、不处理中断的应用在写入音频数据前,使用audioRenderer.state检查播放器状态。而订阅音频中断事件,可以获取到更多详细信息,具体可参考[InterruptEvent](../reference/apis/js-apis-audio.md#interruptevent9)
需要说明的是,本模块的订阅音频中断事件与[AudioManager](../reference/apis/js-apis-audio.md#audiomanager)模块中的on('interrupt')稍有不同。自api9以来,on('interrupt')和off('interrupt')均被废弃。在AudioRenderer模块,当开发者需要监听焦点变化事件时,只需要调用on('audioInterrupt')函数,当应用内部的AudioRenderer对象在start\stop\pause等动作发生时,会主动请求焦点,从而发生焦点转移,相关的AudioRenderer对象即可获取到对应的回调信息。但对除AudioRenderer的其他对象,例如FM、语音唤醒等,应用不会创建对象,此时可调用AudioManager中的on('interrupt')获取焦点变化通知。
```js
async subscribeAudioRender(){
this.audioRenderer.on('audioInterrupt', (interruptEvent) => {
console.info('InterruptEvent Received');
console.info(`InterruptType: ${interruptEvent.eventType}`);
console.info(`InterruptForceType: ${interruptEvent.forceType}`);
console.info(`AInterruptHint: ${interruptEvent.hintType}`);
if (interruptEvent.forceType == audio.InterruptForceType.INTERRUPT_FORCE) {
switch (interruptEvent.hintType) {
// 音频框架发起的强制暂停操作,为防止数据丢失,此时应该停止数据的写操作
case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
console.info('isPlay is false');
break;
// 音频框架发起的强制停止操作,为防止数据丢失,此时应该停止数据的写操作
case audio.InterruptHint.INTERRUPT_HINT_STOP:
console.info('isPlay is false');
break;
// 音频框架发起的强制降低音量操作
case audio.InterruptHint.INTERRUPT_HINT_DUCK:
break;
// 音频框架发起的恢复音量操作
case audio.InterruptHint.INTERRUPT_HINT_UNDUCK:
break;
}
} else if (interruptEvent.forceType == audio.InterruptForceType.INTERRUPT_SHARE) {
switch (interruptEvent.hintType) {
// 提醒App开始渲染
case audio.InterruptHint.INTERRUPT_HINT_RESUME:
this.startRenderer();
break;
// 提醒App音频流被中断,由App自主决定是否继续(此处选择暂停)
case audio.InterruptHint.INTERRUPT_HINT_PAUSE:
console.info('isPlay is false');
this.pauseRenderer();
break;
}
}
});
}
```
10. (可选)使用on('markReach')方法订阅渲染器标记到达事件,使用off('markReach')取消订阅事件。
注册markReach监听后,当渲染器渲染的帧数到达设定值时,会触发回调并返回设定的值。
```js
async markReach(){
this.audioRenderer.on('markReach', 50, (position) => {
if (position == 50) {
console.info('ON Triggered successfully');
}
});
this.audioRenderer.off('markReach'); // 取消markReach事件的订阅,后续将无法监听到“标记到达”事件
}
```
11. (可选)使用on('periodReach')方法订阅渲染器区间标记到达事件,使用off('periodReach')取消订阅事件。
注册periodReach监听后,**每当**渲染器渲染的帧数到达设定值时,会触发回调并返回设定的值。
```js
async periodReach(){
this.audioRenderer.on('periodReach',10, (reachNumber) => {
console.info(`In this period, the renderer reached frame: ${reachNumber} `);
});
this.audioRenderer.off('periodReach'); // 取消periodReach事件的订阅,后续将无法监听到“区间标记到达”事件
}
```
12. (可选)使用on('stateChange')方法订阅渲染器音频状态变化事件。
注册stateChange监听后,当渲染器的状态发生改变时,会触发回调并返回当前渲染器的状态。
```js
async stateChange(){
this.audioRenderer.on('stateChange', (audioState) => {
console.info('State change event Received');
console.info(`Current renderer state is: ${audioState}`);
});
}
```
13. (可选)对on()方法的异常处理。
在使用on()方法时,如果传入的字符串错误或传入的参数类型错误,程序会抛出异常,需要用try catch来捕获。
```js
async errorCall(){
try {
this.audioRenderer.on('invalidInput', () => { // 字符串不匹配
})
} catch (err) {
console.info(`Call on function error, ${err}`); // 程序抛出401异常
}
try {
this.audioRenderer.on(1, () => { // 入参类型错误
})
} catch (err) {
console.info(`Call on function error, ${err}`); // 程序抛出6800101异常
}
}
```
14. (可选)on('audioInterrupt')方法完整示例。
请注意:在调用前声明audioRenderer1与audioRenderer2对象,具体请参考步骤1。
同一个应用中的AudioRender1和AudioRender2在创建时均设置了焦点模式为独立,并且调用on('audioInterrupt')监听焦点变化。刚开始AudioRender1拥有焦点,当AudioRender2获取到焦点时,audioRenderer1将收到焦点转移的通知,打印相关日志。如果AudioRender1和AudioRender2不将焦点模式设置为独立,则监听处理中的日志在应用运行过程中永远不会被打印。
```js
async runningAudioRender1(){
let audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S32LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
}
let audioRendererInfo = {
content: audio.ContentType.CONTENT_TYPE_MUSIC,
usage: audio.StreamUsage.STREAM_USAGE_MEDIA,
rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0
}
let audioRendererOptions = {
streamInfo: audioStreamInfo,
rendererInfo: audioRendererInfo
}
//1.1 创建对象
this.audioRenderer1 = await audio.createAudioRenderer(audioRendererOptions);
console.info("Create audio renderer 1 success.");
//1.2 设置焦点模式为独立模式 :1
this.audioRenderer1.setInterruptMode(1).then( data => {
console.info('audioRenderer1 setInterruptMode Success!');
}).catch((err) => {
console.error(`audioRenderer1 setInterruptMode Fail: ${err}`);
});
//1.3 设置监听
this.audioRenderer1.on('audioInterrupt', async(interruptEvent) => {
console.info(`audioRenderer1 on audioInterrupt : ${JSON.stringify(interruptEvent)}`)
});
//1.4 启动渲染
await this.audioRenderer1.start();
console.info('startAudioRender1 success');
//1.5 获取缓存区大小,此处是渲染器的合理的最小缓冲区大小(也可以选择其它大小的缓冲区)
const bufferSize = await this.audioRenderer1.getBufferSize();
console.info(`audio bufferSize: ${bufferSize}`);
//1.6 获取原始音频数据文件
let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径
const path1 = dir + '/music001_48000_32_1.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/music001_48000_32_1.wav
console.info(`audioRender1 file path: ${ path1}`);
let file1 = fs.openSync(path1, fs.OpenMode.READ_ONLY);
let stat = await fs.stat(path1); //音乐文件信息
let buf = new ArrayBuffer(bufferSize);
let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1);
//1.7 通过audioRender对缓存区的原始音频数据进行渲染
for (let i = 0;i < len; i++) {
let options = {
offset: i * this.bufferSize,
length: this.bufferSize
}
let readsize = await fs.read(file1.fd, buf, options)
let writeSize = await new Promise((resolve,reject)=>{
this.audioRenderer1.write(buf,(err,writeSize)=>{
if(err){
reject(err)
}else{
resolve(writeSize)
}
})
})
}
fs.close(file1)
await this.audioRenderer1.stop(); //停止渲染
await this.audioRenderer1.release(); //释放资源
}
async runningAudioRender2(){
let audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S32LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
}
let audioRendererInfo = {
content: audio.ContentType.CONTENT_TYPE_MUSIC,
usage: audio.StreamUsage.STREAM_USAGE_MEDIA,
rendererFlags: 0 // 0是音频渲染器的扩展标志位,默认为0
}
let audioRendererOptions = {
streamInfo: audioStreamInfo,
rendererInfo: audioRendererInfo
}
//2.1 创建对象
this.audioRenderer2 = await audio.createAudioRenderer(audioRendererOptions);
console.info("Create audio renderer 2 success.");
//2.2 设置焦点模式为独立模式 :1
this.audioRenderer2.setInterruptMode(1).then( data => {
console.info('audioRenderer2 setInterruptMode Success!');
}).catch((err) => {
console.error(`audioRenderer2 setInterruptMode Fail: ${err}`);
});
//2.3 设置监听
this.audioRenderer2.on('audioInterrupt', async(interruptEvent) => {
console.info(`audioRenderer2 on audioInterrupt : ${JSON.stringify(interruptEvent)}`)
});
//2.4 启动渲染
await this.audioRenderer2.start();
console.info('startAudioRender2 success');
//2.5 获取缓存区大小
const bufferSize = await this.audioRenderer2.getBufferSize();
console.info(`audio bufferSize: ${bufferSize}`);
//2.6 获取原始音频数据文件
let dir = globalThis.fileDir; //不可直接访问,没权限,切记!!!一定要使用沙箱路径
const path2 = dir + '/music002_48000_32_1.wav'; // 需要渲染的音乐文件 实际路径为:/data/storage/el2/base/haps/entry/files/music002_48000_32_1.wav
console.info(`audioRender2 file path: ${ path2}`);
let file2 = fs.openSync(path2, fs.OpenMode.READ_ONLY);
let stat = await fs.stat(path2); //音乐文件信息
let buf = new ArrayBuffer(bufferSize);
let len = stat.size % this.bufferSize == 0 ? Math.floor(stat.size / this.bufferSize) : Math.floor(stat.size / this.bufferSize + 1);
//2.7 通过audioRender对缓存区的原始音频数据进行渲染
for (let i = 0;i < len; i++) {
let options = {
offset: i * this.bufferSize,
length: this.bufferSize
}
let readsize = await fs.read(file2.fd, buf, options)
let writeSize = await new Promise((resolve,reject)=>{
this.audioRenderer2.write(buf,(err,writeSize)=>{
if(err){
reject(err)
}else{
resolve(writeSize)
}
})
})
}
fs.close(file2)
await this.audioRenderer2.stop(); //停止渲染
await this.audioRenderer2.release(); //释放资源
}
//综合调用入口
async test(){
await this.runningAudioRender1();
await this.runningAudioRender2();
}
```
\ No newline at end of file
# 路由、设备管理开发指导
## 简介
AudioRoutingManager提供了音频路由、设备管理的方法。开发者可以通过本指导了解应用如何通过AudioRoutingManager获取当前工作的输入、输出音频设备,监听音频设备的连接状态变化,激活通信设备等。
## 运作机制
该模块提供了路由、设备管理模块常用接口
**图1** 路由、设备管理常用接口
![zh-ch_image_audio_routing_manager](figures/zh-ch_image_audio_routing_manager.png)
**说明:** AudioRoutingManager主要接口有:获取设备列表信息、监听与取消监听设备连接状态、激活通信设备、查询通信设备激活状态。更多介绍请参考[API参考文档](../reference/apis/js-apis-audio.md)
## 开发指导
详细API含义可参考:[音频路由、设备管理API文档AudioRoutingManager](../reference/apis/js-apis-audio.md#audioroutingmanager9)
1. 创建AudioRoutingManager实例。
在使用AudioRoutingManager的API前,需要使用getRoutingManager创建一个AudioRoutingManager实例。
```js
import audio from '@ohos.multimedia.audio';
async loadAudioRoutingManager() {
var audioRoutingManager = await audio.getAudioManager().getRoutingManager();
console.info('audioRoutingManager------create-------success.');
}
```
2. (可选)获取设备列表信息、监听设备链接状态变化。
如果开发者需要获取设备列表信息(输入、输出、分布式输入、分布式输出等),或者监听音频设备的链接状态变化时,可参考并调用以下接口。
```js
import audio from '@ohos.multimedia.audio';
//创建AudioRoutingManager实例
async loadAudioRoutingManager() {
var audioRoutingManager = await audio.getAudioManager().getRoutingManager();
console.info('audioRoutingManager------create-------success.');
}
//获取全部音频设备信息(开发者可以根据自身需要填入适当的DeviceFlag)
async getDevices() {
await loadAudioRoutingManager();
await audioRoutingManager.getDevices(audio.DeviceFlag.ALL_DEVICES_FLAG).then((data) => {
console.info(`getDevices success and data is: ${JSON.stringify(data)}.`);
});
}
//监听音频设备状态变化
async onDeviceChange() {
await loadAudioRoutingManager();
await audioRoutingManager.on('deviceChange', audio.DeviceFlag.ALL_DEVICES_FLAG, (deviceChanged) => {
console.info('on device change type : ' + deviceChanged.type);
console.info('on device descriptor size : ' + deviceChanged.deviceDescriptors.length);
console.info('on device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceRole);
console.info('on device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceType);
});
}
//取消监听音频设备状态变化
async offDeviceChange() {
await loadAudioRoutingManager();
await audioRoutingManager.off('deviceChange', (deviceChanged) => {
console.info('off device change type : ' + deviceChanged.type);
console.info('off device descriptor size : ' + deviceChanged.deviceDescriptors.length);
console.info('off device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceRole);
console.info('off device change descriptor : ' + deviceChanged.deviceDescriptors[0].deviceType);
});
}
//综合调用:先查询所有设备,设置监听,然后开发者手动变更设备连接(例如有线耳机),再次查询所有设备,最后取消设备状态变化的监听。
async test(){
await getDevices();
await onDeviceChange()();
//开发者手动断开/连接设备
await getDevices();
await offDeviceChange();
}
```
3. (可选)设置通信设备激活并查询激活状态。
```js
import audio from '@ohos.multimedia.audio';
//创建AudioRoutingManager实例
async loadAudioRoutingManager() {
var audioRoutingManager = await audio.getAudioManager().getRoutingManager();
console.info('audioRoutingManager------create-------success.');
}
//设置通信设备激活状态
async setCommunicationDevice() {
await loadAudioRoutingManager();
await audioRoutingManager.setCommunicationDevice(audio.CommunicationDeviceType.SPEAKER, true).then(() => {
console.info('setCommunicationDevice true is success.');
});
}
//查询通信设备激活状态
async isCommunicationDeviceActive() {
await loadAudioRoutingManager();
await audioRoutingManager.isCommunicationDeviceActive(audio.CommunicationDeviceType.SPEAKER).then((value) => {
console.info(`CommunicationDevice state is: ${value}.`);
});
}
//综合调用:先设置设备激活,然后查询设备状态。
async test(){
await setCommunicationDevice();
await isCommunicationDeviceActive();
}
```
# 音频流管理开发指导
## 简介
AudioStreamManager提供了音频流管理的方法。开发者可以通过本指导了解应用如何通过AudioStreamManager管理音频流。
## 运作机制
该模块提供了音频流管理调用关系图
**图1** 音频流管理调用关系图
![zh-ch_image_audio_stream_manager](figures/zh-ch_image_audio_stream_manager.png)
**说明**:在进行应用开发的过程中,开发者需要使用getStreamManager()创建一个AudioStreamManager实例,进而通过该实例管理音频流。开发者可通过调用on('audioRendererChange')、on('audioCapturerChange')监听音频播放应用和音频录制应用,在应用状态变化、设备变化、音频属性变化时获得通知。同时可通过off('audioRendererChange')、off('audioCapturerChange')取消相关事件的监听。与此同时,开发者可以通过调用(可选)使用getCurrentAudioRendererInfoArray()获取当前音频播放应用的音频流唯一ID、音频播放客户端的UID、音频状态等信息,同理可调用getCurrentAudioCapturerInfoArray()获取音频录制应用的信息。
## 开发指导
详细API含义可参考:[音频管理API文档AudioStreamManager](../reference/apis/js-apis-audio.md#audiostreammanager9)
1. 创建AudioStreamManager实例。
在使用AudioStreamManager的API前,需要使用getStreamManager()创建一个AudioStreamManager实例。
```js
var audioManager = audio.getAudioManager();
var audioStreamManager = audioManager.getStreamManager();
```
2. (可选)使用on('audioRendererChange')监听音频渲染器更改事件。
如果音频流监听应用需要在音频播放应用状态变化、设备变化、音频属性变化时获取通知,可以订阅该事件。更多事件请参考[API参考文档](../reference/apis/js-apis-audio.md)
```js
audioStreamManager.on('audioRendererChange', (AudioRendererChangeInfoArray) => {
for (let i = 0; i < AudioRendererChangeInfoArray.length; i++) {
AudioRendererChangeInfo = AudioRendererChangeInfoArray[i];
console.info('## RendererChange on is called for ' + i + ' ##');
console.info('StreamId for ' + i + ' is:' + AudioRendererChangeInfo.streamId);
console.info('ClientUid for ' + i + ' is:' + AudioRendererChangeInfo.clientUid);
console.info('Content for ' + i + ' is:' + AudioRendererChangeInfo.rendererInfo.content);
console.info('Stream for ' + i + ' is:' + AudioRendererChangeInfo.rendererInfo.usage);
console.info('Flag ' + i + ' is:' + AudioRendererChangeInfo.rendererInfo.rendererFlags);
console.info('State for ' + i + ' is:' + AudioRendererChangeInfo.rendererState);
var devDescriptor = AudioRendererChangeInfo.deviceDescriptors;
for (let j = 0; j < AudioRendererChangeInfo.deviceDescriptors.length; j++) {
console.info('Id:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].id);
console.info('Type:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].deviceType);
console.info('Role:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].deviceRole);
console.info('Name:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].name);
console.info('Address:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].address);
console.info('SampleRates:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].sampleRates[0]);
console.info('ChannelCounts' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].channelCounts[0]);
console.info('ChannelMask:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].channelMasks);
}
}
});
```
3. (可选)使用off('audioRendererChange')取消监听音频渲染器更改事件。
```js
audioStreamManager.off('audioRendererChange');
console.info('######### RendererChange Off is called #########');
```
4. (可选)使用on('audioCapturerChange')监听音频捕获器更改事件。
如果音频流监听应用需要在音频录制应用状态变化、设备变化、音频属性变化时获取通知,可以订阅该事件。更多事件请参考[API参考文档](../reference/apis/js-apis-audio.md)
```js
audioStreamManager.on('audioCapturerChange', (AudioCapturerChangeInfoArray) => {
for (let i = 0; i < AudioCapturerChangeInfoArray.length; i++) {
console.info(' ## audioCapturerChange on is called for element ' + i + ' ##');
console.info('StreamId for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].streamId);
console.info('ClientUid for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].clientUid);
console.info('Source for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerInfo.source);
console.info('Flag ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerInfo.capturerFlags);
console.info('State for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerState);
for (let j = 0; j < AudioCapturerChangeInfoArray[i].deviceDescriptors.length; j++) {
console.info('Id:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].id);
console.info('Type:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceType);
console.info('Role:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceRole);
console.info('Name:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].name);
console.info('Address:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].address);
console.info('SampleRates:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].sampleRates[0]);
console.info('ChannelCounts' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelCounts[0]);
console.info('ChannelMask:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelMasks);
}
}
});
```
5. (可选)使用off('audioCapturerChange')取消监听音频捕获器更改事件。
```js
audioStreamManager.off('audioCapturerChange');
console.info('######### CapturerChange Off is called #########');
```
6. (可选)使用getCurrentAudioRendererInfoArray()获取当前音频渲染器的信息。
该接口可获取音频流唯一ID,音频播放客户端的UID,音频状态以及音频播放器的其他信息。需注意的是若对第三方音频流监听应用未配置ohos.permission.USE_BLUETOOTH权限,则查询到的设备名称和设备地址为空字符串,若正确配置权限,则显示的实际的设备名称和设备地址信息。
```js
await audioStreamManager.getCurrentAudioRendererInfoArray().then( function (AudioRendererChangeInfoArray) {
console.info('######### Get Promise is called ##########');
if (AudioRendererChangeInfoArray != null) {
for (let i = 0; i < AudioRendererChangeInfoArray.length; i++) {
AudioRendererChangeInfo = AudioRendererChangeInfoArray[i];
console.info('StreamId for ' + i +' is:' + AudioRendererChangeInfo.streamId);
console.info('ClientUid for ' + i + ' is:' + AudioRendererChangeInfo.clientUid);
console.info('Content ' + i + ' is:' + AudioRendererChangeInfo.rendererInfo.content);
console.info('Stream' + i +' is:' + AudioRendererChangeInfo.rendererInfo.usage);
console.info('Flag' + i + ' is:' + AudioRendererChangeInfo.rendererInfo.rendererFlags);
console.info('State for ' + i + ' is:' + AudioRendererChangeInfo.rendererState);
var devDescriptor = AudioRendererChangeInfo.deviceDescriptors;
for (let j = 0; j < AudioRendererChangeInfo.deviceDescriptors.length; j++) {
console.info('Id:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].id);
console.info('Type:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].deviceType);
console.info('Role:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].deviceRole);
console.info('Name:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].name);
console.info('Address:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].address);
console.info('SampleRates:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].sampleRates[0]);
console.info('ChannelCounts' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].channelCounts[0]);
console.info('ChannelMask:' + i + ':' + AudioRendererChangeInfo.deviceDescriptors[j].channelMasks);
}
}
}
}).catch((err) => {
console.log('getCurrentAudioRendererInfoArray :ERROR: ' + err.message);
});
```
7. (可选)使用getCurrentAudioCapturerInfoArray()获取当前音频捕获器的信息。
该接口可获取音频流唯一ID,音频录制客户端的UID,音频状态以及音频捕获器的其他信息。需注意的是若对第三方音频流监听应用未配置ohos.permission.USE_BLUETOOTH权限,则查询到的设备名称和设备地址为空字符串,若正确配置权限,则显示的实际的设备名称和设备地址信息。
```js
await audioStreamManager.getCurrentAudioCapturerInfoArray().then( function (AudioCapturerChangeInfoArray) {
console.info('getCurrentAudioCapturerInfoArray: **** Get Promise Called ****');
if (AudioCapturerChangeInfoArray != null) {
for (let i = 0; i < AudioCapturerChangeInfoArray.length; i++) {
console.info('StreamId for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].streamId);
console.info('ClientUid for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].clientUid);
console.info('Source for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerInfo.source);
console.info('Flag ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerInfo.capturerFlags);
console.info('State for ' + i + 'is:' + AudioCapturerChangeInfoArray[i].capturerState);
var devDescriptor = AudioCapturerChangeInfoArray[i].deviceDescriptors;
for (let j = 0; j < AudioCapturerChangeInfoArray[i].deviceDescriptors.length; j++) {
console.info('Id:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].id);
console.info('Type:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceType);
console.info('Role:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].deviceRole);
console.info('Name:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].name)
console.info('Address:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].address);
console.info('SampleRates:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].sampleRates[0]);
console.info('ChannelCounts' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelCounts[0]);
console.info('ChannelMask:' + i + ':' + AudioCapturerChangeInfoArray[i].deviceDescriptors[j].channelMasks);
}
}
}
}).catch((err) => {
console.log('getCurrentAudioCapturerInfoArray :ERROR: ' + err.message);
});
```
\ No newline at end of file
# 音量管理开发指导
## 简介
AudioVolumeManager提供了音量管理的方法。开发者可以通过本指导了解应用如何通过AudioVolumeManager获取指定流音量信息、监听铃声模式变化、设置麦克风静音等。
## 运作机制
该模块提供了音量管理模块常用接口
**图1** 音量管理常用接口
![zh-ch_image_audio_volume_manager](figures/zh-ch_image_audio_volume_manager.png)
**说明:** AudioVolumeManager包含音量变化监听处理和音频音量组管理相关(AudioVolumeGroupManager),开发者调用AudioVolumeGroupManager的相关方法,需要先调用getVolumeGroupManager方法创建AudioVolumeGroupManager实例,从而调用对应的接口实现相应的功能,主要接口有:获取指定流的音量、设置麦克风静音、监听麦克风状态变化等。更多介绍请参考[API参考文档](../reference/apis/js-apis-audio.md)
## 约束与限制
开发者在进行麦克风管理开发前,需要先对所开发的应用配置麦克风权限(ohos.permission.MICROPHONE),如果要设置麦克风状态,则需要配置音频管理配置权限(ohos.permission.MANAGE_AUDIO_CONFIG),需注意该权限为系统级别权限。配置方式请参见[访问控制授权申请](../security/accesstoken-guidelines.md#配置文件权限声明)
## 开发指导
详细API含义可参考:[音量管理API文档AudioVolumeManager](../reference/apis/js-apis-audio.md#audiovolumemanager9)
1. 创建AudioVolumeGroupManager实例。
在使用AudioVolumeGroupManager的API前,需要使用getVolumeGroupManager创建一个AudioStreamManager实例。
```js
import audio from '@ohos.multimedia.audio';
async loadVolumeGroupManager() {
const groupid = audio.DEFAULT_VOLUME_GROUP_ID;
var audioVolumeGroupManager = await audio.getAudioManager().getVolumeManager().getVolumeGroupManager(groupid);
console.error('audioVolumeGroupManager create success.');
}
```
2. (可选)获取音量信息、铃声模式。
如果开发者需要获取指定音频流的音量信息(铃声、语音电话、媒体、语音助手等),或者获取当前设备是静音、震动、响铃模式,可参考并调用以下接口。更多事件请参考[API参考文档](../reference/apis/js-apis-audio.md)
```js
import audio from '@ohos.multimedia.audio';
async loadVolumeGroupManager() {
const groupid = audio.DEFAULT_VOLUME_GROUP_ID;
var audioVolumeGroupManager = await audio.getAudioManager().getVolumeManager().getVolumeGroupManager(groupid);
console.info('audioVolumeGroupManager create success.');
}
//获取指定流的当前音量(范围为0 ~ 15)
async getVolume() {
await loadVolumeGroupManager();
await audioVolumeGroupManager.getVolume(audio.AudioVolumeType.MEDIA).then((value) => {
console.info(`getVolume success and volume is: ${value}.`);
});
}
//获取指定流的最小音量
async getMinVolume() {
await loadVolumeGroupManager();
await audioVolumeGroupManager.getMinVolume(audio.AudioVolumeType.MEDIA).then((value) => {
console.info(`getMinVolume success and volume is: ${value}.`);
});
}
//获取指定流的最大音量
async getMaxVolume() {
await loadVolumeGroupManager();
await audioVolumeGroupManager.getMaxVolume(audio.AudioVolumeType.MEDIA).then((value) => {
console.info(`getMaxVolume success and volume is: ${value}.`);
});
}
//获取当前铃声模式: 静音(0)| 震动(1) | 响铃(2)
async getRingerMode() {
await loadVolumeGroupManager();
await audioVolumeGroupManager.getRingerMode().then((value) => {
console.info(`getRingerMode success and RingerMode is: ${value}.`);
});
}
```
3. (可选)查询、设置、监听麦克风状态。
如果开发者需要获取、设置麦克风状态,或者监听麦克风状态变化等信息,可参考并调用以下接口。
```js
import audio from '@ohos.multimedia.audio';
async loadVolumeGroupManager() {
const groupid = audio.DEFAULT_VOLUME_GROUP_ID;
var audioVolumeGroupManager = await audio.getAudioManager().getVolumeManager().getVolumeGroupManager(groupid);
console.info('audioVolumeGroupManager create success.');
}
async on() { //监听麦克风状态变化
await loadVolumeGroupManager();
await audioVolumeGroupManager.audioVolumeGroupManager.on('micStateChange', (micStateChange) => {
console.info(`Current microphone status is: ${micStateChange.mute} `);
});
}
async isMicrophoneMute() { //查询麦克风是否静音
await audioVolumeGroupManager.audioVolumeGroupManager.isMicrophoneMute().then((value) => {
console.info(`isMicrophoneMute is: ${value}.`);
});
}
async setMicrophoneMuteTrue() { //设置麦克风静音
await loadVolumeGroupManager();
await audioVolumeGroupManager.audioVolumeGroupManager.setMicrophoneMute(true).then(() => {
console.info('setMicrophoneMute to mute.');
});
}
async setMicrophoneMuteFalse() { //取消麦克风静音
await loadVolumeGroupManager();
await audioVolumeGroupManager.audioVolumeGroupManager.setMicrophoneMute(false).then(() => {
console.info('setMicrophoneMute to not mute.');
});
}
async test(){ //综合调用:先设置监听,然后查询麦克风状态,设置麦克风静音后再查询状态,最后取消麦克风静音。
await on();
await isMicrophoneMute();
await setMicrophoneMuteTrue();
await isMicrophoneMute();
await setMicrophoneMuteFalse();
}
```
# 音视频概述
在音视频开发指导中,将介绍各种涉及音频、视频播放或录制功能场景的开发方式,指导开发者如何使用系统提供的音视频API实现对应功能。比如使用TonePlayer实现简单的提示音,当设备接收到新消息时,会发出短促的“滴滴”声;使用AVPlayer实现音乐播放器,循环播放一首音乐。
在每个功能中,会介绍多种实现方式以应对不同的使用场景,以及该场景相关的子功能点。比如在音频播放功能内,会同时介绍音频的并发策略、音量管理和输出设备等在操作系统中的处理方式,帮助开发者能够开发出功能覆盖更全面的应用。
本开发指导仅针对音视频播放或录制本身,由[@ohos.multimedia.audio](../reference/apis/js-apis-audio.md)(下文简称audio模块)和[@ohos.multimedia.media](../reference/apis/js-apis-media.md)(下文简称media模块)提供相关能力,不涉及UI界面、图形处理、媒体存储或其他相关领域功能。
## 开发说明
在开发音频功能之前,尤其是要实现处理音频数据的功能前,建议开发者先了解声学相关的知识,帮助理解操作系统提供的API是如何控制音频系统,从而开发出更易用、体验更好的音视频类应用。建议了解的相关概念包括但不限于:
- 音频量化的过程:采样 &gt; 量化 &gt; 编码
- 音频量化过程的相关概念:模拟信号和数字信号、采样率、声道、采样格式、位宽、码率、常见编码格式(如AAC、MP3、PCM、WMA等)、常见封装格式(如WAV、MPA、FLAC、AAC、OGG等)
在开发音乐、视频播放功能之前,建议了解流媒体播放的相关概念包括但不限于:
- 播放过程:网络协议 &gt; 容器格式 &gt; 音视频编解码 &gt; 图形/音频渲染
- 网络协议:比如HLS、HTTP/HTTPS;容器格式:比如mp4,mkv,mpeg-ts,webm。
- 编码格式:比如h263/h264/h265,MPEG4/MPEG2。
## 音频流介绍
在开发音频应用之前,还需要了解什么是音频流,它是OpenHarmony音频系统中的关键概念,在之后的章节中会多次提及。
音频流,是音频系统中对一个具备音频格式和音频使用场景信息的独立音频数据处理单元的定义,可以表示播放,也可以表示录制,并且具备独立音量调节和音频设备路由切换能力。
音频流基础信息通过[AudioStreamInfo](../reference/apis/js-apis-audio.md#audiostreaminfo8)表示,包含采样、声道、位宽、编码信息,是创建音频播放或录制流的必要参数,描述了音频数据的基本属性。在配置时开发者需要保证基础信息与传输的音频数据是相匹配的,音频系统才能正确处理数据。
### 音频流使用场景信息
除了基本属性,音频流还需要具备使用场景信息。基础信息只能对音频数据进行描述,但在实际的使用过程中,不同的音频流,在音量大小,设备路由,并发策略上是有区别的。系统就是通过音频流所附带的使用场景信息,为不同的音频流制定合适的处理策略,以达到最佳的音频用户体验。
- 播放场景
音频播放场景的信息,通过[StreamUsage](../reference/apis/js-apis-audio.md#streamusage)[ContentType](../reference/apis/js-apis-audio.md#contenttype)进行描述。
StreamUsage指音频流本身的用途类型,包括媒体、语音通信、语音播报、通知、铃声等。
ContentType指音频流中数据的内容类型,包括语音、音乐、影视、通知、铃声等。
- 录制场景
音频流录制场景的信息,通过[SourceType](../reference/apis/js-apis-audio.md#sourcetype8)进行描述。
SourceType指音频流中录音源的类型,包括麦克风音频源、语音识别音频源、语音通话音频源等。
## 支持的音频格式
audio模块下的接口支持PCM编码,包括AudioRenderer、AudioCapturer、TonePlayer、OpenSL ES等。
音频格式说明:
- 支持的常用的音频采样率(Hz):8000、11025、12000、16000、22050、24000、32000、44100、48000、64000、96000,具体参考枚举[AudioSamplingRate](../reference/apis/js-apis-audio.md#audiosamplingrate8)
不同设备支持的采样率规格会存在差异。
- 支持单声道、双声道,具体参考[AudioChannel](../reference/apis/js-apis-audio.md#audiochannel8)
- 支持的采样格式:U8(无符号8位整数)、S16LE(带符号的16位整数,小尾数)、S24LE(带符号的24位整数,小尾数)、S32LE(带符号的32位整数,小尾数)、F32LE(带符号的32位浮点数,小尾数),具体参考[AudioSampleFormat](../reference/apis/js-apis-audio.md#audiosampleformat8)
由于系统限制,S24LE、S32LE、F32LE仅部分设备支持,请根据实际情况使用。
小尾数指的是小端模式,即数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。这种存储模式将地址的高低和数据的位权有效结合起来,高地址部分权值高,低地址部分权值低。
media模块下的接口支持的音视频格式将在[AVPlayer和AVRecorder](avplayer-avrecorder-overview.md)的介绍中承载。
# AVPlayer和AVRecorder
media模块提供了[AVPlayer](#avplayer)[AVRecorder](#avrecorder)用于播放、录制音视频。
## AVPlayer
AVPlayer主要工作是将Audio/Video媒体资源(比如mp4/mp3/mkv/mpeg-ts等)转码为可供渲染的图像和可听见的音频模拟信号,并通过输出设备进行播放。
AVPlayer提供功能完善一体化播放能力,应用只需要提供流媒体来源,不负责数据解析和解码就可达成播放效果。
### 音频播放
当使用AVPlayer开发音乐应用播放音频时,其交互关系如图所示。
**图1** 音频播放外部模块交互图  
![Audio Playback Interaction Diagram](figures/audio-playback-interaction-diagram.png)
音乐类应用通过调用JS接口层提供的AVPlayer接口实现相应功能时,框架层会通过播放服务(Player Framework)将资源解析成音频数据流(PCM),音频数据流经过软件解码后输出至音频服务(Audio Framework),由音频服务输出至音频驱动渲染,实现音频播放功能。完整的音频播放需要应用、Player Framework、Audio Framework、音频HDI共同实现。
图1中,数字标注表示需要数据与外部模块的传递。
1. 音乐应用将媒体资源传递给AVPlayer接口。
2. Player Framework将音频PCM数据流输出给Audio Framework,再由Audio Framework输出给音频HDI。
### 视频播放
当使用AVPlayer开发视频应用播放视频时,其交互关系如图所示。
**图2** 视频播放外部模块交互图  
![Video playback interaction diagram](figures/video-playback-interaction-diagram.png)
应用通过调用JS接口层提供的AVPlayer接口实现相应功能时,框架层会通过播放服务(Player Framework)解析成单独的音频数据流和视频数据流,音频数据流经过软件解码后输出至音频服务(Audio Framework),再至硬件接口层的音频HDI,实现音频播放功能。视频数据流经过硬件(推荐)/软件解码后输出至图形渲染服务(Graphic Framework),再输出至硬件接口层的显示HDI,完成图形渲染。
完整的视频播放需要:应用、XCompomemt、Player Framework、Graphic Framework、Audio Framework、显示HDI和音频HDI共同实现。
图2中,数字标注表示需要数据与外部模块的传递。
1. 应用从Xcomponent组件获取窗口SurfaceID,获取方式参考[XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md)
2. 应用把媒体资源、SurfaceID传递给AVPlayer接口。
3. Player Framework把视频ES数据流输出给解码HDI,解码获得视频帧(NV12/NV21/RGBA)。
4. Player Framework把音频PCM数据流输出给Audio Framework,Audio Framework输出给音频HDI。
5. Player Framework把视频帧(NV12/NV21/RGBA)输出给Graphic Framework,Graphic Framework输出给显示HDI。
### 支持的格式与协议
推荐使用以下主流的播放格式,音视频容器、音视频编码属于内容创作者所掌握的专业领域,不建议应用开发者自制码流进行测试,以免产生无法播放、卡顿、花屏等兼容性问题。若发生此类问题不会影响系统,退出播放即可。
支持的协议如下:
| 协议类型 | 协议描述 |
| -------- | -------- |
| 本地点播 | 协议格式:支持file&nbsp;descriptor,禁止file&nbsp;path |
| 网络点播 | 协议格式:支持http/https/hls |
支持的音频播放格式如下:
| 音频容器规格 | 规格描述 |
| -------- | -------- |
| m4a | 音频格式:AAC |
| aac | 音频格式:AAC |
| mp3 | 音频格式:MP3 |
| ogg | 音频格式:VORBIS |
| wav | 音频格式:PCM |
> **说明:**
>
> 视频播放支持的视频格式分为必选规格和可选规格。必选规格为所有厂商均支持的视频格式。对于可选规格,厂商将基于实际情况决定是否实现。建议开发者做对应的兼容处理,保证应用功能全平台兼容。
| 视频格式 | 是否必选规格 |
| -------- | -------- |
| H264 | 是 |
| MPEG2 | 否 |
| MPEG4 | 否 |
| H263 | 否 |
| VP8 | 否 |
支持的视频播放格式和主流分辨率如下:
| 视频容器规格 | 规格描述 | 分辨率 |
| -------- | -------- | -------- |
| mp4 | 视频格式:H264/MPEG2/MPEG4/H263<br/>音频格式:AAC/MP3 | 主流分辨率,如4K/1080P/720P/480P/270P |
| mkv | 视频格式:H264/MPEG2/MPEG4/H263<br/>音频格式:AAC/MP3 | 主流分辨率,如4K/1080P/720P/480P/270P |
| ts | 视频格式:H264/MPEG2/MPEG4<br/>音频格式:AAC/MP3 | 主流分辨率,如4K/1080P/720P/480P/270P |
| webm | 视频格式:VP8<br/>音频格式:VORBIS | 主流分辨率,如4K/1080P/720P/480P/270P |
## AVRecorder
AVRecorder主要工作是捕获音频信号,接收视频信号,完成音视频编码并保存到文件中,帮助开发者轻松实现音视频录制功能,包括开始录制、暂停录制、恢复录制、停止录制、释放资源等功能控制。它允许调用者指定录制的编码格式、封装格式、文件路径等参数。
**图3** 视频录制外部模块交互图  
![Video recording interaction diagram](figures/video-recording-interaction-diagram.png)
- 音频录制:应用通过调用JS接口层提供的AVRecorder接口实现音频录制时,框架层会通过录制服务(Player Framework),调用音频服务(Audio Framework)通过音频HDI捕获音频数据,通过软件编码封装后保存至文件中,实现音频录制功能。
- 视频录制:应用通过调用JS接口层提供的AVRecorder接口实现视频录制时,先通过Camera接口调用相机服务(Camera Framework)通过视频HDI捕获图像数据送至框架层的录制服务,录制服务将图像数据通过视频编码HDI编码,再将编码后的图像数据封装至文件中,实现视频录制功能。
通过音视频录制组合,可分别实现纯音频录制、纯视频录制,音视频录制。
图3中,数字标注表示需要数据与外部模块的传递。
1. 应用通过AVRecorder接口从录制服务获取SurfaceID。
2. 应用将SurfaceID设置给相机服务,相机服务可以通过SurfaceID获取到Surface。相机服务通过视频HDI捕获图像数据送至框架层的媒体服务。
3. 相机服务通过Surface将视频数据传递给录制服务。
4. 录制服务通过视频编码HDI模块将视频数据编码。
5. 录制服务将音频参数设置给音频服务,并从音频服务获取到音频数据。
### 支持的格式
支持的音频源如下:
| 音频源类型 | 说明 |
| -------- | -------- |
| mic | 系统麦克风作为音频源输入。 |
支持的视频源如下:
| 视频源类型 | 说明 |
| -------- | -------- |
| surface_yuv | 输入surface中携带的是raw&nbsp;data。 |
| surface_es | 输入surface中携带的是ES&nbsp;data。 |
支持的音视频编码格式如下:
| 音视频编码格式 | 说明 |
| -------- | -------- |
| audio/mp4a-latm | 音频/mp4a-latm类型 |
| video/mp4v-es | 视频/mpeg4类型 |
| video/avc | 视频/avc类型 |
支持的输出文件格式如下:
| 输出文件格式 | 说明 |
| -------- | -------- |
| mp4 | 视频的容器格式,MP4。 |
| m4a | 音频的容器格式,M4A。 |
# AVPlayer播放器开发指导
## 简介
AVPlayer主要工作是将Audio/Video媒体资源转码为可供渲染的图像和可听见的音频模拟信号,并通过输出设备进行播放,同时对播放任务进行管理,包括开始播放、暂停播放、停止播放、释放资源、设置音量、跳转播放位置、获取轨道信息等功能控制。
## 运作机制
该模块提供了播放状态变化示意图[AVPlayerState](../reference/apis/js-apis-media.md#avplayerstate9)、音频播放外部模块交互图和视频播放外部模块交互图。
**图1** 播放状态变化示意图<a name = avplayer_state></a>
![zh-ch_image_avplayer_state_machine](figures/zh-ch_image_avplayer_state_machine.png)
**图2** 音频播放外部模块交互图
![zh-ch_image_avplayer_audio](figures/zh-ch_image_avplayer_audio.png)
**说明**:应用通过调用JS接口层提供的AVPlayer js接口实现相应功能时,框架层会通过Player Framework的播放服务解析成音频数据流,音频数据流经过软件解码后输出至Audio Framework的音频服务,由音频子系统输出至硬件接口层的音频HDI,实现音频播放功能。完整的音乐播放器工作需要:应用(应用适配)、Player Framework、Audio Framework、Audio HDI(驱动适配)共同实现。
*注意:音频播放需要音频子系统配合*
1. 应用把url传递给AVPlayer JS。
2. 播放服务把音频PCM数据流输出给音频服务,音频服务输出给Audio HDI。
**图3** 视频播放外部模块交互图
![zh-ch_image_avplayer_video](figures/zh-ch_image_avplayer_video.png)
**说明**:应用通过调用JS接口层提供的AVPlayer js接口实现相应功能时,框架层会通过Player Framework的播放服务解析成单独的音频数据流和视频数据流,音频数据流经过软件解码后输出至Audio Framework的音频服务,由音频子系统输出至硬件接口层的音频HDI,实现音频播放功能。视频数据流经过硬件(推荐)/软件解码后输出至Graphic Framework的渲染服务(Renderer Service),由RS子系统输出至硬件接口层的显示HDI。完整的视频播放器工作需要:应用(应用适配)、XCompomemt组件、Player Framework、Graphic Framework、Audio Framework、Display HDI(驱动适配)和Audio HDI(驱动适配)共同实现。
*注意:视频播放需要显示、音频、解码等多个子系统配合。*
1. 应用从Xcomponent组件获取surfaceID,[获取方式](../reference/arkui-ts/ts-basic-components-xcomponent.md)
2. 应用把url、surfaceID传递给AVPlayer JS。
3. 播放服务把视频ES数据流输出给Codec HDI,解码获得视频帧(NV12/NV21/RGBA)。
4. 播放服务把音频PCM数据流输出给音频服务,音频服务输出给Audio HDI。
5. 播放服务把视频帧(NV12/NV21/RGBA)输出给RS服务,RS服务输出给Display HDI。
## 兼容性说明
视频播放支持的视频格式分必选规格和可选规格。必选规格为所有厂商均支持的视频格式。对于可选规格,厂商将基于实际情况决定是否实现。建议开发者做兼容处理,保证全平台兼容。
推荐使用主流的播放格式和主流分辨率,不建议开发者自制非常或者异常码流,以免产生无法播放、卡住、花屏等兼容性问题。若发生此类问题不会影响系统,退出码流播放即可。
| 视频格式 | 是否必选规格 |
|:--------:|:-----:|
| H264 | 是 |
| MPEG2 | 否 |
| MPEG4 | 否 |
| H263 | 否 |
| VP8 | 否 |
主流的播放格式和主流分辨率如下:
| 视频容器规格 | 规格描述 | 分辨率 |
| :----------: | :-----------------------------------------------: | :--------------------------------: |
| mp4 | 视频格式:H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P |
| mkv | 视频格式:H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P |
| ts | 视频格式:H264/MPEG2/MPEG4 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P |
| webm | 视频格式:VP8 音频格式:VORBIS | 主流分辨率,如1080P/720P/480P/270P |
| 音频容器规格 | 规格描述 |
| :----------: | :----------: |
| m4a | 音频格式:AAC |
| aac | 音频格式:AAC |
| mp3 | 音频格式:MP3 |
| ogg | 音频格式:VORBIS |
| wav | 音频格式:PCM |
## 开发指导
详细API含义可参考:[媒体服务API文档AVPlayer](../reference/apis/js-apis-media.md#avplayer9)
### 播放流程说明
播放的全流程场景包含:创建实例,设置资源,设置窗口(视频),准备播放(获取轨道信息/音量/倍速/焦点模式/缩放模式/设置bitrates),播控(播放/暂停/Seek/音量/停止),重置资源,销毁播放
1:创建实例[createAVPlayer()](../reference/apis/js-apis-media.md#mediacreateavplayer9),AVPlayer初始化[idle](#avplayer_state)状态
2:设置业务需要的监听事件,搭配全流程场景使用
3:设置资源 [url](../reference/apis/js-apis-media.md#avplayer_属性),AVPlayer进入[initialized](#avplayer_state)状态,此时可以设置视频窗口 [surfaceId](../reference/apis/js-apis-media.md#avplayer_属性),支持的规格可参考:[AVPlayer属性说明](../reference/apis/js-apis-media.md#avplayer_属性)
4:准备播放 [prepare()](../reference/apis/js-apis-media.md#avplayer_prepare),AVPlayer进入[prepared](#avplayer_state)状态
5:视频播控:播放 [play()](../reference/apis/js-apis-media.md#avplayer_play),暂停 [pause()](../reference/apis/js-apis-media.md#avplayer_pause),跳转 [seek()](../reference/apis/js-apis-media.md#avplayer_seek),停止 [stop()](../reference/apis/js-apis-media.md#avplayer_stop) 等操作
6:重置资源 [reset()](../reference/apis/js-apis-media.md#avplayer_reset),AVPlayer重新进入[idle](#avplayer_state)状态,允许更换资源 [url](../reference/apis/js-apis-media.md#avplayer_属性)
7:销毁播放 [release()](../reference/apis/js-apis-media.md#avplayer_release),AVPlayer进入[released](#avplayer_state)状态,退出播放
> **说明:**
>
> prepared/playing/paused/compeled 状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存,当客户端暂时不使用播放器时,要求调用 reset() 或 release() 回收。
### 监听事件
| 事件类型 | 说明 |
| ------------------------------------------------- | ------------------------------------------------------------ |
| stateChange<a name = stateChange></a> | 必要事件,监听播放器的状态机 |
| error<a name = error></a> | 必要事件,监听播放器的错误信息 |
| durationUpdate<a name = durationUpdate></a> | 用于进度条,监听进度条长度,刷新资源时长 |
| timeUpdate<a name = timeUpdate></a> | 用于进度条,监听进度条当前位置,刷新当前时间 |
| seekDone<a name = seekDone></a> | 响应api调用,监听seek()请求完成情况 |
| speedDone<a name = speedDone></a> | 响应api调用,监听setSpeed()请求完成情况 |
| volumeChange<a name = volumeChange></a> | 响应api调用,监听setVolume()请求完成情况 |
| bitrateDone<a name = bitrateDone></a> | 响应api调用,用于HLS协议流,监听setBitrate()请求完成情况 |
| availableBitrates<a name = availableBitrates></a> | 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate() |
| bufferingUpdate<a name = bufferingUpdate></a> | 用于网络播放,监听网络播放缓冲信息 |
| startRenderFrame<a name = startRenderFrame></a> | 用于视频播放,监听视频播放首帧渲染时间 |
| videoSizeChange<a name = videoSizeChange></a> | 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例 |
| audioInterrupt<a name = audioInterrupt></a> | 用于视频播放,监听音频焦点切换信息,搭配属性audioInterruptMode使用 |
###
### 全量接口示例
```js
import media from '@ohos.multimedia.media'
import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs'
const TAG = 'AVPlayerDemo:'
export class AVPlayerDemo {
private count:number = 0
private avPlayer
private surfaceID:string // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法
// 注册avplayer回调函数
setAVPlayerCallback() {
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state, reason) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info(TAG + 'state idle called')
this.avPlayer.release() // 释放avplayer对象
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.info(TAG + 'state initialized called ')
this.avPlayer.surfaceId = this.surfaceID // 设置显示画面,当播放的资源为纯音频时无需设置
this.avPlayer.prepare().then(() => {
console.info(TAG+ 'prepare success');
}, (err) => {
console.error(TAG + 'prepare filed,error message is :' + err.message)
})
break;
case 'prepared': // prepare调用成功后上报该状态机
console.info(TAG + 'state prepared called')
this.avPlayer.play() // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
console.info(TAG + 'state playing called')
if (this.count == 0) {
this.avPlayer.pause() // 调用暂停播放接口
} else {
this.avPlayer.seek(10000, media.SeekMode.SEEK_PREV_SYNC) // 前向seek置10秒处,触发seekDone回调函数
}
break;
case 'paused': // pause成功调用后触发该状态机上报
console.info(TAG + 'state paused called')
if (this.count == 0) {
this.count++
this.avPlayer.play() // 继续调用播放接口开始播放
}
break;
case 'completed': // 播放结束后触发该状态机上报
console.info(TAG + 'state completed called')
this.avPlayer.stop() //调用播放结束接口
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
console.info(TAG + 'state stopped called')
this.avPlayer.reset() // 调用reset接口初始化avplayer状态
break;
case 'released':
console.info(TAG + 'state released called')
break;
case 'error':
console.info(TAG + 'state error called')
break;
default:
console.info(TAG + 'unkown state :' + state)
break;
}
})
// 时间上报监听函数
this.avPlayer.on('timeUpdate', (time:number) => {
console.info(TAG + 'timeUpdate success,and new time is :' + time)
})
// 音量变化回调函数
this.avPlayer.on('volumeChange', (vol:number) => {
console.info(TAG + 'volumeChange success,and new volume is :' + vol)
this.avPlayer.setSpeed(media.AVPlayerSpeed.SPEED_FORWARD_2_00_X) // 设置两倍速播放,并触发speedDone回调
})
// 视频播放结束触发回调
this.avPlayer.on('endOfStream', () => {
console.info(TAG + 'endOfStream success')
})
// seek操作回调函数
this.avPlayer.on('seekDone', (seekDoneTime:number) => {
console.info(TAG + 'seekDone success,and seek time is:' + seekDoneTime)
this.avPlayer.setVolume(0.5) // 设置音量为0.5,并触发volumeChange回调函数
})
// 设置倍速播放回调函数
this.avPlayer.on('speedDone', (speed:number) => {
console.info(TAG + 'speedDone success,and speed value is:' + speed)
})
// bitrate设置成功回调函数
this.avPlayer.on('bitrateDone', (bitrate:number) => {
console.info(TAG + 'bitrateDone success,and bitrate value is:' + bitrate)
})
// 缓冲上报回调函数
this.avPlayer.on('bufferingUpdate', (infoType: media.BufferingInfoType, value: number) => {
console.info(TAG + 'bufferingUpdate success,and infoType value is:' + infoType + ', value is :' + value)
})
// 首帧上报回调函数
this.avPlayer.on('startRenderFrame', () => {
console.info(TAG + 'startRenderFrame success')
})
// 视频宽高上报回调函数
this.avPlayer.on('videoSizeChange', (width: number, height: number) => {
console.info(TAG + 'videoSizeChange success,and width is:' + width + ', height is :' + height)
})
// 焦点上报回调函数
this.avPlayer.on('audioInterrupt', (info: audio.InterruptEvent) => {
console.info(TAG + 'audioInterrupt success,and InterruptEvent info is:' + info)
})
// HLS上报所有支持的比特率
this.avPlayer.on('availableBitrates', (bitrates: Array<number>) => {
console.info(TAG + 'availableBitrates success,and availableBitrates length is:' + bitrates.length)
})
}
async avPlayerDemo() {
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer()
let fdPath = 'fd://'
let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。
// path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el2/100/base/ohos.acts.multimedia.media.avplayer/haps/entry/files" 命令,将其推送到设备上
let path = pathDir + '/H264_AAC.mp4'
let file = await fs.open(path)
fdPath = fdPath + '' + file.fd
this.avPlayer.url = fdPath
}
}
```
### 正常播放场景
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
const TAG = 'AVPlayerDemo:'
export class AVPlayerDemo {
private avPlayer
private surfaceID:string // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法
// 注册avplayer回调函数
setAVPlayerCallback() {
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state, reason) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info(TAG + 'state idle called')
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.info(TAG + 'state initialized called ')
this.avPlayer.surfaceId = this.surfaceID // 设置显示画面,当播放的资源为纯音频时无需设置
this.avPlayer.prepare().then(() => {
console.info(TAG+ 'prepare success');
}, (err) => {
console.error(TAG + 'prepare filed,error message is :' + err.message)
})
break;
case 'prepared': // prepare调用成功后上报该状态机
console.info(TAG + 'state prepared called')
this.avPlayer.play() // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
console.info(TAG + 'state playing called')
break;
case 'paused': // pause成功调用后触发该状态机上报
console.info(TAG + 'state paused called')
break;
case 'completed': // 播放结束后触发该状态机上报
console.info(TAG + 'state completed called')
this.avPlayer.stop() //调用播放结束接口
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
console.info(TAG + 'state stopped called')
this.avPlayer.release() // 调用reset接口初始化avplayer状态
break;
case 'released':
console.info(TAG + 'state released called')
break;
case 'error':
console.info(TAG + 'state error called')
break;
default:
console.info(TAG + 'unkown state :' + state)
break;
}
})
}
async avPlayerDemo() {
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer()
let fileDescriptor = undefined
// 使用资源管理模块的getRawFileDescriptor获取集成在应用中的媒体资源,并使用AVPlayer的fdSrc属性完成媒体资源初始化
// 其中的参数fd/offset/length定义请查看媒体API文档,globalThis.abilityContext参数为系统环境变量,在系统启动时在主界面保存为全局变量
await globalThis.abilityContext.resourceManager.getRawFileDescriptor('H264_AAC.mp4').then((value) => {
fileDescriptor = {fd: value.fd, offset: value.offset, length: value.length}
})
this.avPlayer.fdSrc = fileDescriptor
}
}
```
### 单曲循环场景
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
const TAG = 'AVPlayerDemo:'
export class AVPlayerDemo {
private count:number = 0
private avPlayer
private surfaceID:string // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法
// 注册avplayer回调函数
setAVPlayerCallback() {
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state, reason) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info(TAG + 'state idle called')
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.info(TAG + 'state initialized called ')
this.avPlayer.surfaceId = this.surfaceID // 设置显示画面,当播放的资源为纯音频时无需设置
this.avPlayer.prepare().then(() => {
console.info(TAG+ 'prepare success');
}, (err) => {
console.error(TAG + 'prepare filed,error message is :' + err.message)
})
break;
case 'prepared': // prepare调用成功后上报该状态机
console.info(TAG + 'state prepared called')
this.avPlayer.loop = true // 设置单曲循环播放,单曲循环播放至结尾后会触发endOfStream回调
this.avPlayer.play() // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
console.info(TAG + 'state playing called')
break;
case 'paused': // pause成功调用后触发该状态机上报
console.info(TAG + 'state paused called')
break;
case 'completed': // 播放结束后触发该状态机上报
console.info(TAG + 'state completed called')
// 当第二次触发endOfStream回调后取消循环播放,再次播放到结尾后触发completed状态机上报
this.avPlayer.stop() //调用播放结束接口
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
console.info(TAG + 'state stopped called')
this.avPlayer.release() // 调用reset接口初始化avplayer状态
break;
case 'released':
console.info(TAG + 'state released called')
break;
case 'error':
console.info(TAG + 'state error called')
break;
default:
console.info(TAG + 'unkown state :' + state)
break;
}
})
// 视频播放结束触发回调
this.avPlayer.on('endOfStream', () => {
console.info(TAG + 'endOfStream success')
if (this.count == 1) {
this.avPlayer.loop = false // 取消循环播放
} else {
this.count++
}
})
}
async avPlayerDemo() {
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer()
let fdPath = 'fd://'
let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。
// path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el2/100/base/ohos.acts.multimedia.media.avplayer/haps/entry/files" 命令,将其推送到设备上
let path = pathDir + '/H264_AAC.mp4'
let file = await fs.open(path)
fdPath = fdPath + '' + file.fd
this.avPlayer.url = fdPath
}
}
```
### 视频切换场景
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
const TAG = 'AVPlayerDemo:'
export class AVPlayerDemo {
private count:number = 0
private avPlayer
private surfaceID:string // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法
async nextVideo() {
let fdPath = 'fd://'
let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。
// path路径的码流可通过"hdc file send D:\xxx\H264_MP3.mp4 /data/app/el2/100/base/ohos.acts.multimedia.media.avplayer/haps/entry/files" 命令,将其推送到设备上
let path = pathDir + '/H264_MP3.mp4'
let file = await fs.open(path)
fdPath = fdPath + '' + file.fd
this.avPlayer.url = fdPath // 再次触发initialized状态机上报
}
// 注册avplayer回调函数
setAVPlayerCallback() {
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state, reason) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info(TAG + 'state idle called')
await this.nextVideo() // 切换下一个视频播放
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.info(TAG + 'state initialized called ')
this.avPlayer.surfaceId = this.surfaceID // 设置显示画面,当播放的资源为纯音频时无需设置
this.avPlayer.prepare().then(() => {
console.info(TAG+ 'prepare success');
}, (err) => {
console.error(TAG + 'prepare filed,error message is :' + err.message)
})
break;
case 'prepared': // prepare调用成功后上报该状态机
console.info(TAG + 'state prepared called')
this.avPlayer.play() // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
console.info(TAG + 'state playing called')
break;
case 'paused': // pause成功调用后触发该状态机上报
console.info(TAG + 'state paused called')
break;
case 'completed': // 播放结束后触发该状态机上报
console.info(TAG + 'state completed called')
if (this.count == 0) {
this.count++
this.avPlayer.reset() //调用重置接口准备切换下一个视频
} else {
this.avPlayer.release() //切换视频后播放至结尾释放avplayer对象
}
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
console.info(TAG + 'state stopped called')
break;
case 'released':
console.info(TAG + 'state released called')
break;
case 'error':
console.info(TAG + 'state error called')
break;
default:
console.info(TAG + 'unkown state :' + state)
break;
}
})
}
async avPlayerDemo() {
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer()
let fdPath = 'fd://'
let pathDir = "/data/storage/el2/base/haps/entry/files" // pathDir在FA模型和Stage模型的获取方式不同,请参考开发步骤首行的说明,根据实际情况自行获取。
// path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el2/100/base/ohos.acts.multimedia.media.avplayer/haps/entry/files" 命令,将其推送到设备上
let path = pathDir + '/H264_AAC.mp4'
let file = await fs.open(path)
fdPath = fdPath + '' + file.fd
this.avPlayer.url = fdPath
}
}
```
\ No newline at end of file
# 音视频录制开发指导
## 简介
音视频录制的主要工作是捕获音频信号,接收视频信号,完成音视频编码并保存到文件中,帮助开发者轻松实现音视频录制功能,包括开始录制、暂停录制、恢复录制、停止录制、释放资源等功能控制。它允许调用者指定录制的编码格式、封装格式、文件路径等参数。
## 运作机制
该模块提供了音视频录制状态变化示意图和音视频录制外部模块交互图。
**图1** 音视频录制状态变化示意图
![zh-ch_image_video_recorder_state_machine](figures/zh-ch_image_avrecorder_state_machine.png)
**图2** 视频录制外部模块交互图--待修改
![zh-ch_image_video_recorder_zero](figures/zh-ch_image_avrecorder_module_interaction.png)
**说明**:音频录制时,框架层会通过Native Framework的媒体服务,调用音频子系统通过音频HDI捕获音频数据,通过软件编码封装后保存至文件中,实现音频录制功能。视频录制时,由相机子系统通过视频HDI捕获图像数据,媒体服务将图像数据通过视频编码HDI编码,再将编码后的图像数据封装至文件中,实现视频录制功能。通过音视频录制组合,可分别实现纯音频录制、纯视频录制,音视频录制。
## 约束与限制
开发者在进行录制功能开发前,需要先对所开发的应用配置相应权限。涉及音频录制,需要获取麦克风权限(ohos.permission.MICROPHONE),权限配置相关内容可参考:[访问控制权限申请指导](../security/accesstoken-guidelines.md)
使用相机进行视频录制还需要与相机模块配合,接口使用以及权限获取详见[相机管理](../reference/apis/js-apis-camera.md)
## 开发指导
详细API含义可参考:[媒体服务API文档AVRecorder](../reference/apis/js-apis-media.md#avrecorder9)
媒体库相关流程含义可参考:[媒体库管理](../reference/apis/js-apis-medialibrary.md)
相机相关流程含义可参考:[相机管理](../reference/apis/js-apis-camera.md)
### 音视频录制全流程场景
音视频录制全流程场景包含:创建实例、设置录制参数、获取输入surface、开始录制、暂停录制、恢复录制、停止录制、释放资源等流程。
音频录制相关配置参数范围,受到设备编解码性能,音频子系统性能等综合限制。
视频录制相关配置参数范围,受到设备编解码性能,相机子系统性能等综合限制。
```
import media from '@ohos.multimedia.media'
import camera from '@ohos.multimedia.camera'
import mediaLibrary from '@ohos.multimedia.mediaLibrary'
export class AVRecorderDemo {
private testFdNumber; // 用于保存fd地址
// 获取录制的音频文件fileName对应的fd,需借助媒体库能力。使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA.
async getFd(fileName) {
// 实现方式参考媒体库资料文档。
this.testFdNumber = "fd://" + fdNumber.toString(); // e.g. fd://54
}
// 当promise接口发生错误上上报的错误回调接口
failureCallback(error) {
console.info('error happened, error message is ' + error.message);
}
// 当promise接口发生异常时,系统调用的错误回调接口
catchCallback(error) {
console.info('catch error happened, error message is ' + error.message);
}
async AVRecorderDemo() {
let AVRecorder; // AVRecorder空对象在createAVRecorder成功后赋值
let surfaceID; // 从getInputSurface获取surfaceID,传递给相机的videoOutput
await this.getFd('01.mp4');
// 音视频录制相关参数配置,配置参数以实际硬件设备支持的范围为准
let avProfile = {
audioBitrate : 48000,
audioChannels : 2,
audioCodec : media.CodecMimeType.AUDIO_AAC,
audioSampleRate : 48000,
fileFormat : media.ContainerFormatType.CFT_MPEG_4,
videoBitrate : 2000000,
videoCodec : media.CodecMimeType.VIDEO_AVC,
videoFrameWidth : 640,
videoFrameHeight : 480,
videoFrameRate : 30
}
let avConfig = {
audioSourceType : media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
videoSourceType : media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
profile : avProfile,
url : 'fd://',
rotation : 0,
location : { latitude : 30, longitude : 130 }
}
// 创建AVRecorder对象
await media.createAVRecorder().then((recorder) => {
console.info('case createAVRecorder called');
if (typeof (recorder) != 'undefined') {
AVRecorder = recorder;
console.info('createAVRecorder success');
} else {
console.info('createAVRecorder failed');
}
}, this.failureCallback).catch(this.catchCallback);
// 对象创建成功后创建on('stateChange')和on('error')监听回调用于监听状态机变化和错误上报
AVRecorder.on('stateChange', async (state, reason) => {
console.info('case state has changed, new state is :' + state);
switch (state) {
// 用户可以根据需求在不同状态设置自己想要进行的行为
case 'idle':
// 调用rest接口后触发idle状态;create后也在idle状态
break;
case 'prepared':
// 调用prepare接口后触发prepared状态;
break;
case 'started':
// 调用start接口后触发started状态;
break;
case 'paused':
// 调用pause接口后触发paused状态;
break;
case 'stopped':
// 调用stop接口后触发stopped状态;
break;
case 'released':
// 调用release接口后触发released状态;
break;
case 'error':
// error状态说明底层出错,用户需排查错误,重新创建avRecorder;
break;
default:
console.info('case state is unknown');
}
});
AVRecorder.on('error', (err) => {
// 监听非接口类错误上报
console.info('case avRecorder.on(error) called, errMessage is ' + err.message);
});
// 调用prepare完成音频录制前的准备工作;底层实际是根据prepare的入参来判断是音频录制、视频录制还是音视频录制
await AVRecorder.prepare(avConfig).then(() => {
console.info('prepare success');
}, this.failureCallback).catch(this.catchCallback);
// 包含视频的录制需要调用getInputSurface接口,并将返回值surfaceID传递给camera相关接口
await AVRecorder.getInputSurface().then((surface) => {
console.info('getInputSurface success');
surfaceID = surface; // surfaceID给camera的createVideoOutput()作为其中的一个入参
}, this.failureCallback).catch(this.catchCallback);
// 视频录制依赖相机相关接口,以下需要先调用相机起流接口后才能继续执行,具体的相机接口调用请参考sample用例
// 视频录制启动接口
await AVRecorder.start().then(() => {
console.info('start success');
}, this.failureCallback).catch(this.catchCallback);
// 调用pause接口时需要暂停camera出流
await AVRecorder.pause().then(() => {
console.info('pause success');
}, this.failureCallback).catch(this.catchCallback);
// 调用resume接口时需要恢复camera出流
await AVRecorder.resume().then(() => {
console.info('resume success');
}, this.failureCallback).catch(this.catchCallback);
// 停止camera出流后,停止视频录制
await AVRecorder.stop().then(() => {
console.info('stop success');
}, this.failureCallback).catch(this.catchCallback);
// 重置录制相关配置
await AVRecorder.reset().then(() => {
console.info('reset success');
}, this.failureCallback).catch(this.catchCallback);
// 关闭监听回调,如果用户不自行调用off接口,在调用release后,设置的回调接口也会无效
AVRecorder.off('stateChange');
AVRecorder.off('error');
// 释放视频录制相关资源并释放camera对象相关资源
await AVRecorder.release().then(() => {
console.info('release success');
}, this.failureCallback).catch(this.catchCallback);
// 相关对象置null
AVRecorder = undefined;
surfaceID = undefined;
}
}
```
### 纯音频录制全流程场景
纯音频录制全流程场景包含:创建实例、设置录制参数、开始录制、暂停录制、恢复录制、停止录制、释放资源等流程。
音频录制相关配置参数范围,受到设备编解码性能,音频子系统性能等综合限制。
```
import media from '@ohos.multimedia.media'
import mediaLibrary from '@ohos.multimedia.mediaLibrary'
export class AudioRecorderDemo {
private testFdNumber; // 用于保存fd地址
// 获取录制的音频文件fileName对应的fd,需借助媒体库能力。使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA
async getFd(fileName) {
// 实现方式参考媒体库资料文档。
this.testFdNumber = "fd://" + fdNumber.toString(); // e.g. fd://54
}
// 当promise接口发生错误上报的错误回调接口
failureCallback(error) {
console.info('error happened, error message is ' + error.message);
}
// 当promise接口发生异常时,系统调用的错误回调接口
catchCallback(error) {
console.info('catch error happened, error message is ' + error.message);
}
async audioRecorderDemo() {
let audioRecorder; // audioRecorder空对象在createAVRecorder成功后赋值
await this.getFd('01.m4a');
// 音频录制相关参数配置
let audioProfile = {
audioBitrate : 48000,
audioChannels : 2,
audioCodec : media.CodecMimeType.AUDIO_AAC,
audioSampleRate : 48000,
fileFormat : media.ContainerFormatType.CFT_MPEG_4,
}
let audioConfig = {
audioSourceType : media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
profile : audioProfile,
url : this.testFdNumber,
rotation : 0,
location : { latitude : 30, longitude : 130 }
}
// 创建audioRecorder对象
await media.createAVRecorder().then((recorder) => {
console.info('case createAVRecorder called');
if (typeof (recorder) != 'undefined') {
audioRecorder = recorder;
console.info('createAudioRecorder success');
} else {
console.info('createAudioRecorder failed');
}
}, this.failureCallback).catch(this.catchCallback);
// 对象创建成功后创建on('stateChange')和on('error')监听回调用于监听状态机变化和错误上报
audioRecorder.on('stateChange', async (state, reason) => {
console.info('case state has changed, new state is :' + state);
switch (state) {
// 用户可以根据需求在不同状态设置自己想要进行的行为
case 'idle':
// 调用rest接口后触发idle状态;create后也在idle状态
break;
case 'prepared':
// 调用prepare接口后触发prepared状态;
break;
case 'started':
// 调用start接口后触发started状态;
break;
case 'paused':
// 调用pause接口后触发paused状态;
break;
case 'stopped':
// 调用stop接口后触发stopped状态;
break;
case 'released':
// 调用release接口后触发released状态;
break;
case 'error':
// error状态说明底层出错,用户需排查错误,重新创建avRecorder;
break;
default:
console.info('case state is unknown');
}
});
audioRecorder.on('error', (err) => {
// 监听非接口类错误上报
console.info('case avRecorder.on(error) called, errMessage is ' + err.message);
});
// 调用prepare完成音频录制前的准备工作;底层实际是根据prepare的入参来判断是音频录制、视频录制还是音视频录制
await audioRecorder.prepare(audioConfig).then(() => {
console.info('prepare success');
}, this.failureCallback).catch(this.catchCallback);
// 调用start接口启动音频录制
await audioRecorder.start().then(() => {
console.info('start success');
}, this.failureCallback).catch(this.catchCallback);
// 调用pause接口暂停音频录制
await audioRecorder.pause().then(() => {
console.info('pause success');
}, this.failureCallback).catch(this.catchCallback);
// 调用resume接口恢复音频录制
await audioRecorder.resume().then(() => {
console.info('resume success');
}, this.failureCallback).catch(this.catchCallback);
// 调用stop接口停止音频录制
await audioRecorder.stop().then(() => {
console.info('stop success');
}, this.failureCallback).catch(this.catchCallback);
// 调用reset接口重置录制相关配置
await audioRecorder.reset().then(() => {
console.info('reset success');
}, this.failureCallback).catch(this.catchCallback);
// 关闭监听回调,如果用户不自行调用off接口,在调用release后,设置的回调接口也会无效
avRecorder.off('stateChange');
avRecorder.off('error');
// 调用release接口释放音频录制相关资源
await audioRecorder.release().then(() => {
console.info('release success');
}, this.failureCallback).catch(this.catchCallback);
// 相关对象置null
audioRecorder = undefined;
}
}
```
### 纯视频录制全流程场景
纯视频录制全流程场景包含:创建实例、设置录制参数、获取输入surface、开始录制、暂停录制、恢复录制、停止录制、释放资源等流程。
视频录制相关配置参数范围,受到设备编解码性能,相机子系统性能等综合限制。
```
import media from '@ohos.multimedia.media'
import camera from '@ohos.multimedia.camera'
import mediaLibrary from '@ohos.multimedia.mediaLibrary'
export class VideoRecorderDemo {
private testFdNumber; // 用于保存fd地址
// 获取录制的音频文件fileName对应的fd,需借助媒体库能力。使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA.
async getFd(fileName) {
// 实现方式参考媒体库资料文档。
this.testFdNumber = "fd://" + fdNumber.toString(); // e.g. fd://54
}
// 当promise接口发生错误上上报的错误回调接口
failureCallback(error) {
console.info('error happened, error message is ' + error.message);
}
// 当promise接口发生异常时,系统调用的错误回调接口
catchCallback(error) {
console.info('catch error happened, error message is ' + error.message);
}
async videoRecorderDemo() {
let videoRecorder; // videoRecorder空对象在createAVRecorder成功后赋值
let surfaceID; // 从getInputSurface获取surfaceID,传递给相机的videoOutput
await this.getFd('01.mp4');
// 纯视频录制相关参数配置,配置参数以实际硬件设备支持的范围为准
let videoProfile = {
fileFormat : media.ContainerFormatType.CFT_MPEG_4,
videoBitrate : 2000000,
videoCodec : media.CodecMimeType.VIDEO_AVC,
videoFrameWidth : 640,
videoFrameHeight : 480,
videoFrameRate : 30
}
let videoConfig = {
videoSourceType : media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
profile : videoProfile,
url : 'fd://',
rotation : 0,
location : { latitude : 30, longitude : 130 }
}
// 创建videoRecorder对象
await media.createAVRecorder().then((recorder) => {
console.info('case createVideoRecorder called');
if (typeof (recorder) != 'undefined') {
videoRecorder = recorder;
console.info('createVideoRecorder success');
} else {
console.info('createVideoRecorder failed');
}
}, this.failureCallback).catch(this.catchCallback);
// 对象创建成功后创建on('stateChange')和on('error')监听回调用于监听状态机变化和错误上报
videoRecorder.on('stateChange', async (state, reason) => {
console.info('case state has changed, new state is :' + state);
switch (state) {
// 用户可以根据需求在不同状态设置自己想要进行的行为
case 'idle':
// 调用rest接口后触发idle状态;create后也在idle状态
break;
case 'prepared':
// 调用prepare接口后触发prepared状态;
break;
case 'started':
// 调用start接口后触发started状态;
break;
case 'paused':
// 调用pause接口后触发paused状态;
break;
case 'stopped':
// 调用stop接口后触发stopped状态;
break;
case 'released':
// 调用release接口后触发released状态;
break;
case 'error':
// error状态说明底层出错,用户需排查错误,重新创建avRecorder;
break;
default:
console.info('case state is unknown');
}
});
videoRecorder.on('error', (err) => {
// 监听非接口类错误上报
console.info('case avRecorder.on(error) called, errMessage is ' + err.message);
});
// 调用prepare完成音频录制前的准备工作;底层实际是根据prepare的入参来判断是音频录制、视频录制还是音视频录制
await videoRecorder.prepare(videoConfig).then(() => {
console.info('prepare success');
}, this.failureCallback).catch(this.catchCallback);
// 包含视频的录制需要调用getInputSurface接口,并将返回值surfaceID传递给camera相关接口
await videoRecorder.getInputSurface().then((surface) => {
console.info('getInputSurface success');
surfaceID = surface; // surfaceID给camera的createVideoOutput()作为其中的一个入参
}, this.failureCallback).catch(this.catchCallback);
// 视频录制依赖相机相关接口,以下需要先调用相机起流接口后才能继续执行,具体的相机接口调用请参考sample用例
// 视频录制启动接口
await videoRecorder.start().then(() => {
console.info('start success');
}, this.failureCallback).catch(this.catchCallback);
// 调用pause接口时需要暂停camera出流
await videoRecorder.pause().then(() => {
console.info('pause success');
}, this.failureCallback).catch(this.catchCallback);
// 调用resume接口时需要恢复camera出流
await videoRecorder.resume().then(() => {
console.info('resume success');
}, this.failureCallback).catch(this.catchCallback);
// 停止camera出流后,停止视频录制
await videoRecorder.stop().then(() => {
console.info('stop success');
}, this.failureCallback).catch(this.catchCallback);
// 重置录制相关配置
await videoRecorder.reset().then(() => {
console.info('reset success');
}, this.failureCallback).catch(this.catchCallback);
// 关闭监听回调,如果用户不自行调用off接口,在调用release后,设置的回调接口也会无效
videoRecorder.off('stateChange');
videoRecorder.off('error');
// 释放视频录制相关资源并释放camera对象相关资源
await videoRecorder.release().then(() => {
console.info('release success');
}, this.failureCallback).catch(this.catchCallback);
// 相关对象置null
videoRecorder = undefined;
surfaceID = undefined;
}
}
```
### 音视频录制APP
音视频录制APP案例包含:创建实例、设置录制参数、获取输入surface、开始录制、暂停录制、恢复录制、停止录制、释放资源等流程。
详细代码可参考:[AVRecorderDemo]([multimedia_player_framework: Implementation of media playback and recording | 媒体播放和录制功能实现 - Gitee.com](https://gitee.com/openharmony/multimedia_player_framework/tree/master/test/sample/AVRecorder))
\ No newline at end of file
# AVSession开发指导
> **说明:**
>
> AVSession的所有接口均为系统接口,其功能仅提供给系统应用使用。
## 会话接入端开发指导
### 基本概念
- 会话元数据`'AVMetadata'`: 媒体数据相关属性,包含标识当前媒体的ID(assetId),上一首媒体的ID(previousAssetId),下一首媒体的ID(nextAssetId),标题(title),专辑作者(author),专辑名称(album),词作者(writer),媒体时长(duration)等属性。
- 会话描述符`'AVSessionDescriptor'`: 描述媒体会话的相关信息。包含标识会话的ID(sessionId),会话的类型type(音频Audio/视频Video),会话自定义名称(sessionTag),会话所属应用的信息(elementName)等。
- 媒体播放状态`'AVPlaybackState'`:用于描述媒体播放状态的相关属性。包含当前媒体的播放状态(state)、位置(position)、速度(speed)、缓冲时间(bufferedTime)、循环模式(loopMode)、是否收藏(isFavorite)等属性。
### 接口说明
会话接入端常用接口如下表所示。接口返回值有两种返回形式:callback和promise,下表中为callback形式接口,promise和callback只是返回值方式不一样,功能相同。更多API说明请参见[API文档](../reference/apis/js-apis-avsession.md)
表1:会话接入端常用接口
| 接口名 | 描述 |
|----------------------------------------------------------------------------------|-------------|
| createAVSession(context: Context, tag: string, type: AVSessionType, callback: AsyncCallback\<AVSession>): void | 创建会话 |
| setAVMetadata(data: AVMetadata, callback: AsyncCallback\<void>): void | 设置会话元数据 |
| setAVPlaybackState(state: AVPlaybackState, callback: AsyncCallback\<void>): void | 设置会话播放状态信息 |
| setLaunchAbility(ability: WantAgent, callback: AsyncCallback\<void>): void | 设置启动ability |
| getController(callback: AsyncCallback\<AVSessionController>): void | 获取当前会话自身控制器 |
| getOutputDevice(callback: AsyncCallback\<OutputDeviceInfo>): void | 获取音频输出设备信息 |
| activate(callback: AsyncCallback\<void>): void | 激活会话 |
| destroy(callback: AsyncCallback\<void>): void | 销毁会话 |
### 开发步骤
1.导入模块接口
```js
import avSession from '@ohos.multimedia.avsession';
import wantAgent from '@ohos.app.ability.wantAgent';
import featureAbility from '@ohos.ability.featureAbility';
```
2.创建会话并激活会话
```js
// 全局变量定义
let mediaFavorite = false;
let currentSession = null;
let context = featureAbility.getContext();
// 创建音频类型会话
avSession.createAVSession(context, "AudioAppSample", 'audio').then((session) => {
currentSession = session;
currentSession.activate(); // 激活会话
}).catch((err) => {
console.info(`createAVSession : ERROR : ${err.message}`);
});
```
3.设置AVSession会话信息,包括:
- 设置会话元数据,除了媒体ID必选外,可选设置媒体标题、专辑信息、媒体作者、媒体时长、上一首/下一首媒体ID等。详细的会话元数据信息可参考API文档中的`AVMetadata`
- 设置启动Ability,通过[WantAgent](../reference/apis/js-apis-app-ability-wantAgent.md)的接口实现。WantAgent一般用于封装行为意图信息。
- 设置播放状态。
```js
// 设置会话元数据
let metadata = {
assetId: "121278",
title: "lose yourself",
artist: "Eminem",
author: "ST",
album: "Slim shady",
writer: "ST",
composer: "ST",
duration: 2222,
mediaImage: "https://www.example.com/example.jpg", // 请开发者根据实际情况使用
subtitle: "8 Mile",
description: "Rap",
lyric: "https://www.example.com/example.lrc", // 请开发者根据实际情况使用
previousAssetId: "121277",
nextAssetId: "121279",
};
currentSession.setAVMetadata(metadata).then(() => {
console.info('setAVMetadata successfully');
}).catch((err) => {
console.info(`setAVMetadata : ERROR : ${err.message}`);
});
```
```js
// 设置启动ability
let wantAgentInfo = {
wants: [
{
bundleName: "com.neu.setResultOnAbilityResultTest1",
abilityName: "com.example.test.EntryAbility",
}
],
operationType: wantAgent.OperationType.START_ABILITIES,
requestCode: 0,
wantAgentFlags:[wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
}
wantAgent.getWantAgent(wantAgentInfo).then((agent) => {
currentSession.setLaunchAbility(agent).then(() => {
console.info('setLaunchAbility successfully');
}).catch((err) => {
console.info(`setLaunchAbility : ERROR : ${err.message}`);
});
});
```
```js
// 设置播放状态
let PlaybackState = {
state: avSession.PlaybackState.PLAYBACK_STATE_STOP,
speed: 1.0,
position:{elapsedTime: 0, updateTime: (new Date()).getTime()},
bufferedTime: 1000,
loopMode: avSession.LoopMode.LOOP_MODE_SEQUENCE,
isFavorite: false,
};
currentSession.setAVPlaybackState(PlaybackState).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
```
```js
// 获取当前session会话对象自身的控制器
currentSession.getController().then((selfController) => {
console.info('getController successfully');
}).catch((err) => {
console.info(`getController : ERROR : ${err.message}`);
});
```
```js
// 获取音频输出设备信息
currentSession.getOutputDevice().then((outputInfo) => {
console.info(`getOutputDevice successfully, deviceName : ${outputInfo.deviceName}`);
}).catch((err) => {
console.info(`getOutputDevice : ERROR : ${err.message}`);
});
```
4.注册控制命令监听
```js
// 注册播放命令监听
currentSession.on('play', () => {
console.log("调用AudioPlayer.play方法");
// 设置播放状态
currentSession.setAVPlaybackState({state: avSession.PlaybackState.PLAYBACK_STATE_PLAY}).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
});
// 注册暂停命令监听
currentSession.on('pause', () => {
console.log("调用AudioPlayer.pause方法");
// 设置播放状态
currentSession.setAVPlaybackState({state: avSession.PlaybackState.PLAYBACK_STATE_PAUSE}).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
});
// 注册停止命令监听
currentSession.on('stop', () => {
console.log("调用AudioPlayer.stop方法");
// 设置播放状态
currentSession.setAVPlaybackState({state: avSession.PlaybackState.PLAYBACK_STATE_STOP}).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
});
// 注册下一首命令监听
currentSession.on('playNext', () => {
// 如果媒体文件未准备好,则下载并缓存媒体文件,设置准备状态
currentSession.setAVPlaybackState({state: avSession.PlaybackState.PLAYBACK_STATE_PREPARE}).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
// 成功获取媒体文件
currentSession.setAVMetadata({assetId: '58970105', title: '不如我们明天见'}).then(() => {
console.info('setAVMetadata successfully');
}).catch((err) => {
console.info(`setAVMetadata : ERROR : ${err.message}`);
});
console.log("调用AudioPlayer.play方法");
// 设置播放状态
let time = (new Date()).getTime();
currentSession.setAVPlaybackState({state: avSession.PlaybackState.PLAYBACK_STATE_PLAY, position: {elapsedTime: 0, updateTime: time}, bufferedTime:2000}).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
});
// 注册播放快进命令监听
currentSession.on('fastForward', () => {
console.log("调用AudioPlayer的倍速播放");
// 设置播放状态
currentSession.setAVPlaybackState({speed: 2.0}).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
});
// 注册跳播命令监听
currentSession.on('seek', (time) => {
console.log("调用AudioPlayer的seek方法");
// 设置播放状态
currentSession.setAVPlaybackState({position: {elapsedTime: time, updateTime: (new Data()).getTime()}}).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
});
// 注册设置播放速度命令监听
currentSession.on('setSpeed', (speed) => {
console.log(`调用AudioPlayer的倍速播放 ${speed}`);
// 设置播放状态
currentSession.setAVPlaybackState({speed: speed}).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
});
// 注册设置播放循环模式命令监听
currentSession.on('setLoopMode', (mode) => {
console.log(`应用自身切换循环模式 ${mode}`);
// 设置播放状态
currentSession.setAVPlaybackState({loopMode: mode}).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
});
// 注册设置歌曲收藏命令监听
currentSession.on('toggleFavorite', (assetId) => {
console.log(`应用保存当前assetId为喜爱 ${assetId}`);
// 根据上一次的状态进行切换
let favorite = mediaFavorite == false ? true : false;
currentSession.setAVPlaybackState({isFavorite: favorite}).then(() => {
console.info('setAVPlaybackState successfully');
}).catch((err) => {
console.info(`setAVPlaybackState : ERROR : ${err.message}`);
});
mediaFavorite = favorite;
});
// 注册媒体按键命令监听
currentSession.on('handleKeyEvent', (event) => {
console.log(`用户按键 ${event.keyCode}`);
});
// 注册播放设备变化命令监听
currentSession.on('outputDeviceChange', (device) => {
console.log(`输出设备变更,更新显示 ${device.deviceName}`);
});
```
5.释放资源
```js
// 取消注册回调
currentSession.off('play');
currentSession.off('pause');
currentSession.off('stop');
currentSession.off('playNext');
currentSession.off('playPrevious');
currentSession.off('fastForward');
currentSession.off('rewind');
currentSession.off('seek');
currentSession.off('setSpeed');
currentSession.off('setLoopMode');
currentSession.off('toggleFavorite');
currentSession.off('handleKeyEvent');
currentSession.off('outputDeviceChange');
// 去激活session并销毁对象
currentSession.deactivate().then(() => {
currentSession.destroy();
});
```
### 调测验证
在媒体应用上点击播放、暂停、下一首等按键,媒体播放状态出现相应变化。
### 常见问题
1.会话服务端异常
- 现象描述:
会话服务端异常,应用端无法获取服务端的消息响应。如会话服务未运行或者会话服务通信失败。返回错误信息: Session service exception。
- 可能原因:
会话重启过程中服务被杀。
- 解决办法
(1)定时重试,超过3s仍失败时,停止对该会话或者控制器进行操作。
(2)销毁当前会话或者会话控制器,并重新创建,如果重新创建失败,则停止会话相关操作。
2.会话不存在
- 现象描述:
会话对象不存在时,向该会话设置参数或者发送命令。返回错误信息: The session does not exist。
- 可能原因:
会话已被销毁,服务端无会话记录。
- 解决办法
(1)如果在会话被控端产生该错误,请重新创建会话;如果是会话控制端,请停止向该会话发送查询或者控制命令。
(2)如果在会话管理端产生该错误,请重新查询系统当前会话记录,在创建控制器时传入正确的会话ID。
3.会话未激活
- 现象描述:
会话没有激活时,向会话发送控制命令或者事件。。返回错误信息: The session not active。
- 可能原因:
会话处于未激活状态。
- 解决办法
停止发送该命令或事件,监听会话的激活状态,会话激活后恢复发送该命令或事件。
### 相关实例
提供[音乐Demo](https://gitee.com/openharmony/multimedia_av_session/blob/master/test/resource/player_index_js.md)的代码实例
## 会话控制端开发指导(播控中心)
### 基本概念
- 远端投播:将本地媒体投播到远端设备,通过本地控制器发送命令,可控制远端播放行为。
- 发送按键命令:控制器通过发送按键事件的方式控制媒体。
- 发送控制命令:控制器通过发送控制命令的方式控制媒体。
- 发送系统按键命令:应用拥有调用该接口的系统权限,通过发送按键事件的方式控制媒体,仅系统应用可用。
- 发送系统控制命令:应用拥有调用该接口的系统权限,通过发送控制命令的方式控制媒体,仅系统应用可用。
### 接口说明
会话控制端涉及的常用接口如下表所示。接口返回值有两种返回形式:callback和promise,下表中为callback形式接口,promise和callback只是返回值方式不一样,功能相同。更多API说明请参见[API文档](../reference/apis/js-apis-avsession.md)
表2:会话控制端常用接口
| 接口名 | 描述 |
| ------------------------------------------------------------------------------------------------ | ----------------- |
| getAllSessionDescriptors(callback: AsyncCallback\<Array\<Readonly\<AVSessionDescriptor>>>): void | 获取所有会话的描述符 |
| createController(sessionId: string, callback: AsyncCallback\<AVSessionController>): void | 创建控制器 |
| sendAVKeyEvent(event: KeyEvent, callback: AsyncCallback\<void>): void | 发送按键命令 |
| getLaunchAbility(callback: AsyncCallback\<WantAgent>): void | 拉起应用 |
| sendControlCommand(command: AVControlCommand, callback: AsyncCallback\<void>): void | 发送控制命令 |
| sendSystemAVKeyEvent(event: KeyEvent, callback: AsyncCallback\<void>): void | 发送系统按键命令 |
| sendSystemControlCommand(command: AVControlCommand, callback: AsyncCallback\<void>): void | 发送系统控制命令 |
| castAudio(session: SessionToken \| 'all', audioDevices: Array\<audio.AudioDeviceDescriptor>, callback: AsyncCallback\<void>): void | 远端投播 |
### 开发步骤
1.导入模块接口
```js
import avSession from '@ohos.multimedia.avsession';
import {Action, KeyEvent} from '@ohos.multimodalInput.KeyEvent';
import wantAgent from '@ohos.app.ability.wantAgent';
import audio from '@ohos.multimedia.audio';
```
2.获取会话描述符,创建控制器
```js
// 全局变量定义
let g_controller = new Array<avSession.AVSessionController>();
let g_centerSupportCmd:Set<avSession.AVControlCommandType> = new Set(['play', 'pause', 'playNext', 'playPrevious', 'fastForward', 'rewind', 'seek','setSpeed', 'setLoopMode', 'toggleFavorite']);
let g_validCmd:Set<avSession.AVControlCommandType>;
// 获取会话描述符,创建控制器
avSession.getAllSessionDescriptors().then((descriptors) => {
descriptors.forEach((descriptor) => {
avSession.createController(descriptor.sessionId).then((controller) => {
g_controller.push(controller);
}).catch((err) => {
console.error('createController error');
});
});
}).catch((err) => {
console.error('getAllSessionDescriptors error');
});
// 注册会话创建监听,创建控制器
avSession.on('sessionCreate', (session) => {
// 新增会话,需要创建控制器
avSession.createController(session.sessionId).then((controller) => {
g_controller.push(controller);
}).catch((err) => {
console.info(`createController : ERROR : ${err.message}`);
});
});
```
3.监听AVSession会话状态以及AVSession服务变化
```js
// 注册会话激活状态变更监听
controller.on('activeStateChange', (isActive) => {
if (isActive) {
console.log("控制器卡片按键高亮");
} else {
console.log("控制器卡片按键变更为无效");
}
});
// 注册会话销毁监听
controller.on('sessionDestroy', () => {
console.info('on sessionDestroy : SUCCESS ');
controller.destroy().then(() => {
console.info('destroy : SUCCESS ');
}).catch((err) => {
console.info(`destroy : ERROR :${err.message}`);
});
});
// 注册系统会话销毁监听
avSession.on('sessionDestroy', (session) => {
let index = g_controller.findIndex((controller) => {
return controller.sessionId == session.sessionId;
});
if (index != 0) {
g_controller[index].destroy();
g_controller.splice(index, 1);
}
});
// 注册系统最高优先级会话变更监听
avSession.on('topSessionChange', (session) => {
let index = g_controller.findIndex((controller) => {
return controller.sessionId == session.sessionId;
});
// 将该会话显示排到第一个
if (index != 0) {
g_controller.sort((a, b) => {
return a.sessionId == session.sessionId ? -1 : 0;
});
}
});
// 注册服务异常监听
avSession.on('sessionServiceDie', () => {
// 服务端异常,应用清理资源
console.log("服务端异常");
})
```
4.监听AVSession会话信息变化
```js
// 注册元数据更新监听
let metaFilter = ['assetId', 'title', 'description'];
controller.on('metadataChange', metaFilter, (metadata) => {
console.info(`on metadataChange assetId : ${metadata.assetId}`);
});
// 注册播放状态更新监听
let playbackFilter = ['state', 'speed', 'loopMode'];
controller.on('playbackStateChange', playbackFilter, (playbackState) => {
console.info(`on playbackStateChange state : ${playbackState.state}`);
});
// 注册会话支持的命令变更监听
controller.on('validCommandChange', (cmds) => {
console.info(`validCommandChange : SUCCESS : size : ${cmds.size}`);
console.info(`validCommandChange : SUCCESS : cmds : ${cmds.values()}`);
g_validCmd.clear();
for (let c of g_centerSupportCmd) {
if (cmds.has(c)) {
g_validCmd.add(c);
}
}
});
// 注册输出设备变更监听
controller.on('outputDeviceChange', (device) => {
console.info(`on outputDeviceChange device isRemote : ${device.isRemote}`);
});
```
5.控制AVSession会话行为
```js
// 用户点击播放按键:发送控制命令--播放
if (g_validCmd.has('play')) {
controller.sendControlCommand({command:'play'}).then(() => {
console.info('sendControlCommand successfully');
}).catch((err) => {
console.info(`sendControlCommand : ERROR : ${err.message}`);
});
}
// 用户点击循环模式:发送控制命令--单曲循环
if (g_validCmd.has('setLoopMode')) {
controller.sendControlCommand({command: 'setLoopMode', parameter: avSession.LoopMode.LOOP_MODE_SINGLE}).then(() => {
console.info('sendControlCommand successfully');
}).catch((err) => {
console.info(`sendControlCommand : ERROR : ${err.message}`);
});
}
// 发送按键事件
let keyItem = {code: 0x49, pressedTime: 123456789, deviceId: 0};
let event = {action: 2, key: keyItem, keys: [keyItem]};
controller.sendAVKeyEvent(event).then(() => {
console.info('sendAVKeyEvent Successfully');
}).catch((err) => {
console.info(`sendAVKeyEvent : ERROR : ${err.message}`);
});
// 用户点击卡片空白位置拉起应用
controller.getLaunchAbility().then((want) => {
console.log("前台拉起应用");
}).catch((err) => {
console.info(`getLaunchAbility : ERROR : ${err.message}`);
});
// 发送系统媒体按键事件
let keyItem = {code: 0x49, pressedTime: 123456789, deviceId: 0};
let event = {action: 2, key: keyItem, keys: [keyItem]};
avSession.sendSystemAVKeyEvent(event).then(() => {
console.info('sendSystemAVKeyEvent Successfully');
}).catch((err) => {
console.info(`sendSystemAVKeyEvent : ERROR : ${err.message}`);
});
// 发送系统控制命令,系统会把控制命令发送到Top会话中
let avcommand = {command: 'toggleFavorite', parameter: "false"};
avSession.sendSystemControlCommand(avcommand).then(() => {
console.info('sendSystemControlCommand successfully');
}).catch((err) => {
console.info(`sendSystemControlCommand : ERROR : ${err.message}`);
});
// 投播到其他设备
let audioManager = audio.getAudioManager();
let audioDevices;
await audioManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG).then((data) => {
audioDevices = data;
console.info('Promise returned to indicate that the device list is obtained.');
}).catch((err) => {
console.info(`getDevices : ERROR : ${err.message}`);
});
avSession.castAudio('all', audioDevices).then(() => {
console.info('createController : SUCCESS');
}).catch((err) => {
console.info(`createController : ERROR : ${err.message}`);
});
```
6.释放资源
```js
// 取消注册回调
controller.off('metadataChange');
controller.off('playbackStateChange');
controller.off('sessionDestroy');
controller.off('activeStateChange');
controller.off('validCommandChange');
controller.off('outputDeviceChange');
// 销毁controller对象
controller.destroy().then(() => {
console.info('destroy : SUCCESS ');
}).catch((err) => {
console.info(`destroy : ERROR : ${err.message}`);
});
```
### 调测验证
在播控中心点击播放、暂停、下一首等按键,应用播放状态随即发生相应变化。
### 常见问题
1.控制器不存在
- 现象描述:
会话控制器不存在时,向该控制器发送控制命令或者事件。返回错误信息: The session controller does not exist。
- 可能原因:
控制器已被销毁。
- 解决办法
请重新查询系统当前会话记录,并创建对应的会话控制器。
2.远端会话连接失败
- 现象描述:
本端会话与远端会话通信失败。返回错误信息: The remote session connection failed。
- 可能原因:
设备间通信断开。
- 解决办法
停止对该会话发送控制命令,并监听输出设备变化,当输出设备发送变化后恢复发送。
3.无效会话命令
- 现象描述:
会话被控端不支持该被控命令或事件。返回错误信息: Invalid session command。
- 可能原因:
被控端不支持该命令。
- 解决办法
停止发送该命令或事件,并查询被控会话支持的命令集,发送被控端支持的命令。
4.消息过载
- 现象描述:
会话客户端在一段时间内向服务端发送了过多的消息或者命令,引起服务端消息过载。返回错误信息: Command or event overload。
- 可能原因:
服务端消息过载。
- 解决办法
检查自身命令发送是否过于频繁,控制自身查询和控制命令的发送频度。
### 相关实例
提供[播控中心Demo](https://gitee.com/openharmony/multimedia_av_session/blob/master/test/resource/controller_index_js.md)的代码实例
\ No newline at end of file
# AVSession开发概述
# 媒体会话概述
> **说明:**
>
> AVSession的所有接口均为系统接口,其功能仅提供给系统应用使用。
媒体会话(AVSession,Audio &amp; Video Session)服务是OpenHarmony提供的音视频管控服务,用于对系统中所有音视频行为进行统一的管理,例如只允许当前存在一个音频应用处于正在播放状态。
## 简介
音视频类应用接入媒体会话后,可以发送应用的数据(比如正在播放的歌曲、歌曲的播放状态等),用户可以通过系统播控中心、语音助手等应用切换多个应用、多个设备播放。音视频类应用如果不接入媒体会话,将无法在后台播放,在应用进入后台时,会被强制停止播放。
AVSession(Audio & Video Session),音视频会话,即媒体会话。
- 对应用开发者而言,媒体会话提供了将应用内音视频接入系统播控中心的能力。
- 对系统开发者而言,媒体会话提供了对系统音视频应用的媒体信息进行展示和统一的媒体播放控制的能力。
## 基础概念
通过AVSession,可以实现
在开发前,需要先了解以下基础概念
1.统一播控入口,提升用户体验。
- 媒体会话(AVSession)
媒体会话的一端连接被控的音视频应用,另一端连接音视频应用的控制端(如播控中心、语音助手等)。媒体会话提供了音视频应用和音视频应用控制端之间进行信息交换的通道。
当设备上安装了多个音视频类应用时,用户需要切换、进入不同的应用来控制播放。通过接入AVSession,音视频应用可以通过系统的统一播控入口(如播控中心)来控制设备上的媒体播放,无需切换不同应用,提升使用体验。
- 媒体会话提供方
媒体会话提供方指接入媒体会话的音视频应用。音视频应用接入媒体会话后,需要向媒体会话提供播放的媒体信息,例如播放曲目名称、播放状态等。同时,音视频应用需要通过媒体会话接收控制端发出的控制命令并进行正确响应。
2.完善音视频后台管控。
当应用在后台自行启动音频播放时,用户难以快速定位对应的应用,造成体验问题。接入AVSession后,允许应用在后台进行音乐播放,便于用户在播控中心快速找到播放应用。
- 媒体会话控制方
媒体会话控制方指接入媒体会话并具有全局管控音视频行为功能的应用,例如系统播控中心、语音助手等。为便于开发者理解,下文将多处使用OpenHarmony系统应用播放中心,作为媒体会话控制方举例。播控中心等系统应用接入媒体会话后,可以通过监听媒体会话获取最新的媒体信息,也可以通过媒体会话向音视频应用发出控制命令。
## 基本概念
- 媒体会话控制器(AVSessionController)
媒体会话控制器的持有者,一般指媒体会话控制方,可以控制媒体会话提供方的应用播放行为,也可以获取应用的播放信息,还可以监听音视频应用播放状态的变化,用于确保媒体会话信息在音视频应用和播控中间之间的同步。
- 媒体会话
- 媒体会话管理器(AVSessionManager)
媒体会话管理器提供了管理媒体会话的能力,可以创建媒体会话、创建媒体会话控制器、发送系统控制事件,也支持对媒体会话的状态进行监听。
用于应用和播控中心之间进行信息交换的通道。会话的一端连接被控的媒体应用,另一端连接媒体应用的控制端(如播控中心)。应用接入了媒体会话后,可以通过媒体会话将媒体播放信息传递给控制端,并能够接收到控制端发出的控制命令。
- 媒体会话控制器
媒体会话控制器的持有者可以控制接入了AVSession应用的播放行为。通过会话控制器,应用可以对应用进程的播放行为进行控制,支持获取应用的播放信息,发送播放控制命令,也支持监听应用的播放状态等的变化,确保媒体会话信息的同步。
## 媒体会话交互过程
- 播控中心
系统统一的媒体控制中心,会话控制器的持有者。通过控制器发送命令来控制媒体的播放、暂停等。
媒体会话分为本地和分布式两种场景。
## 实现原理
![AVSession Interaction Process](figures/avsession-interaction-process.png)
该模块提供了`AVSession`会话类和`AVSessionController`控制器类。
- 本地媒体会话
本地媒体会话在本地设备中的媒体会话提供方和媒体会话控制方之间建立连接,实现系统中音视频应用统一的媒体播放控制和媒体信息显示。
**图1** AVSession交互图
![zh-ch_image_avsession](figures/zh-cn_image_avsession.png)
- 应用与播控中心交互:首先,音频应用创建`AVSession`对象,并设置会话信息(包括媒体元数据、对应拉起的Ability、播放状态等)。然后,播控中心创建`AVSessionController`,可获取会话相关信息,向音频应用发送播放命令。最后,音频应用响应播控中心的命令并更新播放状态。
- 支持分布式投播:当组网内的设备创建本地会话之后,播控中心或者音频应用可以根据设备列表选择想要投播的其他设备,将本地会话同步到远端,生成远端会话,并支持远端控制。需要控制远端会话时,通过远端控制器将控制命令发送到远端会话控制中心。
- 分布式媒体会话
分布式媒体会话在跨设备场景中的媒体会话提供方和媒体会话控制方之间建立连接,实现音视频应用跨设备的媒体播放控制和媒体信息显示。例如,将设备A中播放的内容投播到设备B,并在设备B中进行播放控制。
## 约束和限制
- 播控中心展示的播放信息,依赖媒体应用主动将媒体信息写入到AVSession。
- 播控中心控制媒体应用播放,依赖于媒体应用侧对控制命令的响应。
- AVSession只能够传递媒体播放信息和播放控制指令,不进行信息的展示和控制命令的执行。
- 普通应用不支持开发播控中心端。当普通音视频应用运行在OpenHarmony上时,默认控制端为系统应用播控中心,开发者无需做额外的操作。
- 播控中心为系统应用,当开发者需要基于OpenHarmony开发自己的系统时,可以开发本系统的播控中心应用。
- 为了解决音频在后台播放时用户无法获取到停止音频的入口,影响用户体验,AVSession服务增加了应用后台管控策略,只有应用接入了AVSession,才可以后台播放,否则当应用切后台时系统会强制暂停其音频播放。
媒体会话服务会对系统中的所有音视频应用进行管控,只有接入了媒体会话的音视频应用才可以在后台播放,未接入的应用在退到后台时,将会被强制暂停音频播放。
# 设备输入
在开发一个相机应用前,需要先创建一个独立的相机设备,应用通过调用和控制相机设备,完成预览、拍照和录像等基础操作。
## 开发步骤
详细的API说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)
1. 导入camera接口,接口中提供了相机相关的属性和方法,导入方法如下。
```ts
import camera from '@ohos.multimedia.camera';
```
2. 通过getCameraManager()方法,获取cameraManager对象。
```ts
let cameraManager;
let context: any = getContext(this);
cameraManager = camera.getCameraManager(context)
```
> **说明:**
>
> 如果获取对象失败,说明相机可能被占用或无法使用。如果被占用,须等到相机被释放后才能重新获取。
3. 通过cameraManager类中的getSupportedCameras()方法,获取当前设备支持的相机列表,列表中存储了设备支持的所有相机ID。若列表不为空,则说明列表中的每个ID都支持独立创建相机对象;否则,说明当前设备无可用相机,不可继续后续操作。
```ts
let cameraArray = cameraManager.getSupportedCameras();
if (cameraArray.length <= 0) {
console.error("cameraManager.getSupportedCameras error");
return;
}
for (let index = 0; index < cameraArray.length; index++) {
console.info('cameraId : ' + cameraArray[index].cameraId); // 获取相机ID
console.info('cameraPosition : ' + cameraArray[index].cameraPosition); // 获取相机位置
console.info('cameraType : ' + cameraArray[index].cameraType); // 获取相机类型
console.info('connectionType : ' + cameraArray[index].connectionType); // 获取相机连接类型
}
```
4. 通过getSupportedOutputCapability()方法,获取当前设备支持的所有输出流,如预览流、拍照流等。输出流在CameraOutputCapability中的各个profile字段中。
```ts
// 创建相机输入流
let cameraInput;
try {
cameraInput = cameraManager.createCameraInput(cameraArray[0]);
} catch (error) {
console.error('Failed to createCameraInput errorCode = ' + error.code);
}
// 监听cameraInput错误信息
let cameraDevice = cameraArray[0];
cameraInput.on('error', cameraDevice, (error) => {
console.info(`Camera input error code: ${error.code}`);
})
// 打开相机
await cameraInput.open();
// 获取相机设备支持的输出流能力
let cameraOutputCapability = cameraManager.getSupportedOutputCapability(cameraArray[0]);
if (!cameraOutputCapability) {
console.error("cameraManager.getSupportedOutputCapability error");
return;
}
console.info("outputCapability: " + JSON.stringify(cameraOutputCapability));
```
## 状态监听
在相机应用开发过程中,可以随时监听相机状态,包括新相机的出现、相机的移除、相机的可用状态。在回调函数中,通过相机ID、相机状态这两个参数进行监听,如当有新相机出现时,可以将新相机加入到应用的备用相机中。
通过注册cameraStatus事件,通过回调返回监听结果,callback返回CameraStatusInfo参数,参数的具体内容可参考相机管理器回调接口实例[CameraStatusInfo](../reference/apis/js-apis-camera.md#camerastatusinfo)
```ts
cameraManager.on('cameraStatus', (cameraStatusInfo) => {
console.info(`camera: ${cameraStatusInfo.camera.cameraId}`);
console.info(`status: ${cameraStatusInfo.status}`);
})
```
# 元数据
元数据(Metadata)是对相机返回的图像信息数据的描述和上下文,针对图像信息,提供的更详细的数据,如照片或视频中,识别人像的取景框坐标等信息。
Metadata主要是通过一个TAG(Key),去找对应的Data,用于传递参数和配置信息,减少内存拷贝操作。
## 开发步骤
详细的API说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)
1. 调用CameraOutputCapability类中的supportedMetadataObjectTypes()方法,获取当前设备支持的元数据类型,并通过createMetadataOutput()方法创建元数据输出流。
```ts
let metadataObjectTypes = cameraOutputCapability.supportedMetadataObjectTypes;
let metadataOutput;
try {
metadataOutput = cameraManager.createMetadataOutput(metadataObjectTypes);
} catch (error) {
// 失败返回错误码error.code并处理
console.info(error.code);
}
```
2. 调用start()方法输出metadata数据,接口调用失败时,会返回相应错误码,错误码类型参见CameraErrorCode。
```ts
metadataOutput.start().then(() => {
console.info('Callback returned with metadataOutput started.');
}).catch((err) => {
console.info('Failed to metadataOutput start '+ err.code);
});
```
3. 调用stop方法停止输出metadata数据,接口调用失败会返回相应错误码,错误码类型参见CameraErrorCode。
```ts
metadataOutput.stop().then(() => {
console.info('Callback returned with metadataOutput stopped.');
}).catch((err) => {
console.info('Failed to metadataOutput stop '+ err.code);
});
```
## 状态监听
在相机应用开发过程中,可以随时监听metadata数据以及输出流的状态。
- 通过注册监听获取metadata对象,监听事件固定为metadataObjectsAvailable。检测到有效metadata数据时,callback返回相应的metadata数据信息,metadataOutput创建成功时可监听。
```ts
metadataOutput.on('metadataObjectsAvailable', (metadataObjectArr) => {
console.info(`metadata output metadataObjectsAvailable`);
})
```
> **说明:**
>
> 当前的元数据类型仅支持人脸检测(FACE_DETECTION)功能。元数据信息对象为识别到的人脸区域的矩形信息(Rect),包含矩形区域的左上角x坐标、y坐标和矩形的宽高数据。
- 通过注册回调函数,获取监听metadata流的错误结果,callback返回metadata输出接口。使用错误时返回对应错误码,错误码类型参见CameraErrorCode。
```ts
metadataOutput.on('error', (metadataOutputError) => {
console.info(`Metadata output error code: ${metadataOutputError.code}`);
})
```
# 相机开发概述
开发者通过调用OpenHarmony相机服务提供的接口可以开发相机应用,应用通过访问和操作相机硬件,实现基础操作,如预览、拍照和录像;还可以通过接口组合完成更多操作,如控制闪光灯和曝光时间、对焦或调焦等。
## 开发模型
相机调用摄像头采集、加工图像视频数据,精确控制对应的硬件,灵活输出图像、视频内容,满足多镜头硬件适配(如广角、长焦、TOF)、多业务场景适配(如不同分辨率、不同格式、不同效果)的要求。
相机的工作流程如图所示,可概括为相机输入设备管理、会话管理和相机输出管理三部分。
- 相机设备调用摄像头采集数据,作为相机输入流。
- 会话管理可配置输入流,即选择哪些镜头进行拍摄。另外还可以配置闪光灯、曝光时间、对焦和调焦等参数,实现不同效果的拍摄,从而适配不同的业务场景。应用可以通过切换会话满足不同场景的拍摄需求。
- 配置相机的输出流,即将内容以预览流、拍照流或视频流输出。
**图1** 相机工作流程  
![Camera Workflow](figures/camera-workflow.png)
了解相机工作流程后,建议开发者了解相机的开发模型,便于更好地开发相机应用。
**图2** 相机开发模型  
![Camera Development Model](figures/camera-development-model.png)
相机应用通过控制相机,实现图像显示(预览)、照片保存(拍照)、视频录制(录像)等基础操作。在实现基本操作过程中,相机服务会控制相机设备采集和输出数据,采集的图像数据在相机底层的设备硬件接口(HDI,Hardware Device Interfaces),直接通过BufferQueue传递到具体的功能模块进行处理。BufferQueue在应用开发中无需关注,用于将底层处理的数据及时送到上层进行图像显示。
以视频录制为例进行说明,相机应用在录制视频过程中,媒体录制服务先创建一个视频Surface用于传递数据,并提供给相机服务,相机服务可控制相机设备采集视频数据,生成视频流。采集的数据通过底层相机HDI处理后,通过Surface将视频流传递给媒体录制服务,媒体录制服务对视频数据进行处理后,保存为视频文件,完成视频录制。
# 开发准备
相机应用开发的主要流程包含开发准备、设备输入、会话管理、预览、拍照和录像等。
在开发相机应用时,需要先申请相机相关权限,确保应用拥有访问相机硬件及其他功能的权限,需要的权限如下表。在申请权限前,请保证符合[权限使用的基本原则](../security/accesstoken-overview.md#权限使用的基本原则)
| 权限名 | 说明 | 授权方式 |
| -------- | -------- | -------- |
| ohos.permission.CAMERA | 允许应用使用相机拍摄照片和录制视频。 | user_grant |
| ohos.permission.MICROPHONE | 允许应用使用麦克风(可选)。<br/>如需同时录制音频,需要申请该权限。 | user_grant |
| ohos.permission.WRITE_MEDIA | 允许应用读写用户外部存储中的媒体文件信息(可选)。 | user_grant |
| ohos.permission.READ_MEDIA | 允许应用读取用户外部存储中的媒体文件信息(可选)。 | user_grant |
| ohos.permission.MEDIA_LOCATION | 允许应用访问用户媒体文件中的地理位置信息(可选)。 | user_grant |
以上权限的授权方式均为user_grant(用户授权),即开发者在module.json5文件中配置对应的权限后,需要使用接口[abilityAccessCtrl.requestPermissionsFromUser](../reference/apis/js-apis-abilityAccessCtrl.md#requestpermissionsfromuser9)去校验当前用户是否已授权。如果是,应用可以直接访问/操作目标对象;否则需要弹框向用户申请授权。
具体申请方式及校验方式,请参考[访问控制授权申请指导](../security/accesstoken-guidelines.md/)
> **说明:**
> 即使用户曾被授予过权限,应用在调用此权限保护的接口前,也应该先检查是否有权限。不能把之前授予的状态持久化,因为用户在动态授予后可能通过“设置”取消应用权限。
# 预览
预览是启动相机后看见的画面,通常在拍照和录像前执行。
## 开发步骤
详细的API说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)
1. 创建Surface。
XComponent组件为预览流提供的Surface,而XComponent的能力由UI提供,相关介绍可参考[XComponent组件参考](../reference/arkui-ts/ts-basic-components-xcomponent.md)
```ts
// 创建XComponentController
mXComponentController: XComponentController = new XComponentController;
build() {
Flex() {
// 创建XComponent
XComponent({
id: '',
type: 'surface',
libraryname: '',
controller: this.mXComponentController
})
.onLoad(() => {
// 设置Surface宽高(1920*1080),预览尺寸设置参考前面 previewProfilesArray 获取的当前设备所支持的预览分辨率大小去设置
this.mXComponentController.setXComponentSurfaceSize({surfaceWidth:1920,surfaceHeight:1080});
// 获取Surface ID
globalThis.surfaceId = this.mXComponentController.getXComponentSurfaceId();
})
.width('1920px')
.height('1080px')
}
}
```
2. 通过CameraOutputCapability类中的previewProfiles()方法获取当前设备支持的预览能力,返回previewProfilesArray数组 。通过createPreviewOutput()方法创建预览输出流,其中,createPreviewOutput()方法中的而两个参数分别是previewProfilesArray数组中的第一项和步骤一中获取的surfaceId。
```ts
let previewProfilesArray = cameraOutputCapability.previewProfiles;
let previewOutput;
try {
previewOutput = cameraManager.createPreviewOutput(previewProfilesArray[0], surfaceId);
}
catch (error) {
console.error("Failed to create the PreviewOutput instance." + error);
}
```
3. 使能。通过start()方法输出预览流,接口调用失败会返回相应错误码,错误码类型参见[CameraErrorCode](../reference/apis/js-apis-camera.md#cameraerrorcode)
```ts
previewOutput.start().then(() => {
console.info('Callback returned with previewOutput started.');
}).catch((err) => {
console.info('Failed to previewOutput start '+ err.code);
});
```
## 状态监听
在相机应用开发过程中,可以随时监听预览输出流状态,包括预览流启动、预览流结束、预览流输出错误。
- 通过注册固定的frameStart回调函数获取监听预览启动结果,previewOutput创建成功时即可监听,预览第一次曝光时触发,有该事件返回结果则认为预览流已启动。
```ts
previewOutput.on('frameStart', () => {
console.info('Preview frame started');
})
```
- 通过注册固定的frameEnd回调函数获取监听预览启动结果,previewOutput创建成功时即可监听,预览完成最后一帧时触发,有该事件返回结果则认为预览流已结束。
```ts
previewOutput.on('frameEnd', () => {
console.info('Preview frame ended');
})
```
- 通过注册固定的cameraStatus回调函数获取监听预览输出错误结果,callback返回预览输出接口使用错误时对应的错误码,错误码类型参见[CameraErrorCode](../reference/apis/js-apis-camera.md#cameraerrorcode)
```ts
previewOutput.on('error', (previewOutputError) => {
console.info(`Preview output error code: ${previewOutputError.code}`);
})
```
# 录像实现方案
## 开发流程
在获取到相机支持的输出流能力后,开始创建录像流,开发流程如下。
![Recording Development Process](figures/recording-development-process.png)
## 完整示例
```ts
import camera from '@ohos.multimedia.camera'
import media from '@ohos.multimedia.media'
// 创建CameraManager对象
context: any = getContext(this)
let cameraManager = camera.getCameraManager(this.context)
if (!cameraManager) {
console.error("camera.getCameraManager error")
return;
}
// 监听相机状态变化
cameraManager.on('cameraStatus', (cameraStatusInfo) => {
console.log(`camera : ${cameraStatusInfo.camera.cameraId}`);
console.log(`status: ${cameraStatusInfo.status}`);
})
// 获取相机设备支持的输出流能力
let cameraOutputCap = cameraManager.getSupportedOutputCapability(cameraArray[0]);
if (!cameraOutputCap) {
console.error("cameraManager.getSupportedOutputCapability error")
return;
}
console.log("outputCapability: " + JSON.stringify(cameraOutputCap));
let previewProfilesArray = cameraOutputCap.previewProfiles;
if (!previewProfilesArray) {
console.error("createOutput previewProfilesArray == null || undefined")
}
let photoProfilesArray = cameraOutputCap.photoProfiles;
if (!photoProfilesArray) {
console.error("createOutput photoProfilesArray == null || undefined")
}
let videoProfilesArray = cameraOutputCap.videoProfiles;
if (!videoProfilesArray) {
console.error("createOutput videoProfilesArray == null || undefined")
}
let metadataObjectTypesArray = cameraOutputCap.supportedMetadataObjectTypes;
if (!metadataObjectTypesArray) {
console.error("createOutput metadataObjectTypesArray == null || undefined")
}
// 配置参数以实际硬件设备支持的范围为准
let AVRecorderProfile = {
audioBitrate : 48000,
audioChannels : 2,
audioCodec : media.CodecMimeType.AUDIO_AAC,
audioSampleRate : 48000,
fileFormat : media.ContainerFormatType.CFT_MPEG_4,
videoBitrate : 2000000,
videoCodec : media.CodecMimeType.VIDEO_MPEG4,
videoFrameWidth : 640,
videoFrameHeight : 480,
videoFrameRate : 30
}
let AVRecorderConfig = {
audioSourceType : media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
videoSourceType : media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
profile : AVRecorderProfile,
url : 'fd://', // 文件需先由调用者创建,赋予读写权限,将文件fd传给此参数,eg.fd://45--file:///data/media/01.mp4
rotation : 0, // 合理值0、90、180、270,非合理值prepare接口将报错
location : { latitude : 30, longitude : 130 }
}
let avRecorder
media.createAVRecorder((error, recorder) => {
if (recorder != null) {
avRecorder = recorder;
console.log('createAVRecorder success');
} else {
console.log(`createAVRecorder fail, error:${error}`);
}
});
avRecorder.prepare(AVRecorderConfig, (err) => {
if (err == null) {
console.log('prepare success');
} else {
console.log('prepare failed and error is ' + err.message);
}
})
let videoSurfaceId = null; // 该surfaceID用于传递给相机接口创造videoOutput
avRecorder.getInputSurface((err, surfaceId) => {
if (err == null) {
console.log('getInputSurface success');
videoSurfaceId = surfaceId;
} else {
console.log('getInputSurface failed and error is ' + err.message);
}
});
// 创建VideoOutput对象
let videoOutput
try {
videoOutput = cameraManager.createVideoOutput(videoProfilesArray[0], videoSurfaceId)
} catch (error) {
console.error('Failed to create the videoOutput instance. errorCode = ' + error.code);
}
// 监听视频输出错误信息
videoOutput.on('error', (error) => {
console.log(`Preview output error code: ${error.code}`);
})
//创建会话
let captureSession
try {
captureSession = cameraManager.createCaptureSession()
} catch (error) {
console.error('Failed to create the CaptureSession instance. errorCode = ' + error.code);
}
// 监听session错误信息
captureSession.on('error', (error) => {
console.log(`Capture session error code: ${error.code}`);
})
// 开始配置会话
try {
captureSession.beginConfig()
} catch (error) {
console.error('Failed to beginConfig. errorCode = ' + error.code);
}
// 获取相机列表
let cameraArray = cameraManager.getSupportedCameras();
if (cameraArray.length <= 0) {
console.error("cameraManager.getSupportedCameras error")
return;
}
// 创建相机输入流
let cameraInput
try {
cameraInput = cameraManager.createCameraInput(cameraArray[0]);
} catch (error) {
console.error('Failed to createCameraInput errorCode = ' + error.code);
}
// 监听cameraInput错误信息
let cameraDevice = cameraArray[0];
cameraInput.on('error', cameraDevice, (error) => {
console.log(`Camera input error code: ${error.code}`);
})
// 打开相机
await cameraInput.open();
// 向会话中添加相机输入流
try {
captureSession.addInput(cameraInput)
} catch (error) {
console.error('Failed to addInput. errorCode = ' + error.code);
}
// 创建预览输出流,其中参数 surfaceId 参考下面 XComponent 组件,预览流为XComponent组件提供的surface
let previewOutput
try {
previewOutput = cameraManager.createPreviewOutput(previewProfilesArray[0], surfaceId)
} catch (error) {
console.error("Failed to create the PreviewOutput instance.")
}
// 向会话中添加预览输入流
try {
captureSession.addOutput(previewOutput)
} catch (error) {
console.error('Failed to addOutput(previewOutput). errorCode = ' + error.code);
}
// 向会话中添加录像输出流
try {
captureSession.addOutput(videoOutput)
} catch (error) {
console.error('Failed to addOutput(videoOutput). errorCode = ' + error.code);
}
// 提交会话配置
await captureSession.commitConfig()
// 启动会话
await captureSession.start().then(() => {
console.log('Promise returned to indicate the session start success.');
})
// 启动录像输出流
videoOutput.start(async (err) => {
if (err) {
console.error('Failed to start the video output ${err.message}');
return;
}
console.log('Callback invoked to indicate the video output start success.');
});
// 开始录像
avRecorder.start().then(() => {
console.log('videoRecorder start success');
})
// 停止录像输出流
videoOutput.stop((err) => {
if (err) {
console.error('Failed to stop the video output ${err.message}');
return;
}
console.log('Callback invoked to indicate the video output stop success.');
});
// 停止录像
avRecorder.stop().then(() => {
console.log('stop success');
})
// 停止当前会话
captureSession.stop()
// 释放相机输入流
cameraInput.close()
// 释放预览输出流
previewOutput.release()
// 释放录像输出流
videoOutput.release()
// 释放会话
captureSession.release()
// 会话置空
captureSession = null
```
# 录像
录像也是相机应用的最重要功能之一,录像是循环帧的捕获。对于录像的流畅度,开发者可以参考[拍照](camera-shooting.md)中的步骤4,设置分辨率、闪光灯、焦距、照片质量及旋转角度等信息。
## 开发步骤
详细的API说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)
1. 创建拍照输出流的SurfaceId以及拍照输出的数据,都需要用到系统提供的[media接口](../reference/apis/js-apis-media.md)能力,导入media接口的方法如下。
```ts
import media from '@ohos.multimedia.media';
```
2. 创建Surface。
系统提供的media接口可以创建一个录像AVRecorder实例,通过该实例的getInputSurface方法获取SurfaceId,与录像输出流做关联,处理录像输出流输出的数据。
```ts
let AVRecorder;
media.createAVRecorder((error, recorder) => {
if (recorder != null) {
AVRecorder = recorder;
console.info('createAVRecorder success');
} else {
console.info(`createAVRecorder fail, error:${error}`);
}
});
let videoSurfaceId = null;
AVRecorder.getInputSurface().then((surfaceId) => {
console.info('getInputSurface success');
videoSurfaceId = surfaceId;
}).catch((err) => {
console.info('getInputSurface failed and catch error is ' + err.message);
});
```
3. 创建录像输出流。
通过CameraOutputCapability类中的videoProfiles,可获取当前设备支持的录像输出流。然后,定义创建录像的参数,通过createVideoOutput方法创建录像输出流。
```ts
let videoProfilesArray = cameraOutputCapability.videoProfiles;
if (!videoProfilesArray) {
console.error("createOutput videoProfilesArray == null || undefined");
}
// 创建视频录制的参数
let videoConfig = {
videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
profile: {
fileFormat : media.ContainerFormatType.CFT_MPEG_4, // 视频文件封装格式,只支持MP4
videoBitrate : 100000, // 视频比特率
videoCodec : media.CodecMimeType.VIDEO_MPEG4, // 视频文件编码格式,支持mpeg4和avc两种格式
videoFrameWidth : 640, // 视频分辨率的宽
videoFrameHeight : 480, // 视频分辨率的高
videoFrameRate : 30 // 视频帧率
},
url: 'fd://35',
rotation: 0
}
// 创建avRecorder
let avRecorder;
media.createAVRecorder((error, recorder) => {
if (recorder != null) {
avRecorder = recorder;
console.info('createAVRecorder success');
} else {
console.info(`createAVRecorder fail, error:${error}`);
}
});
// 设置视频录制的参数
avRecorder.prepare(videoConfig);
// 创建VideoOutput对象
let videoOutput;
try {
videoOutput = cameraManager.createVideoOutput(videoProfilesArray[0], videoSurfaceId);
} catch (error) {
console.error('Failed to create the videoOutput instance. errorCode = ' + error.code);
}
```
4. 开始录像。
先通过videoOutput的start方法启动录像输出流,再通过avRecorder的start方法开始录像。
```
videoOutput.start(async (err) => {
if (err) {
console.error('Failed to start the video output ${err.message}');
return;
}
console.info('Callback invoked to indicate the video output start success.');
});
avRecorder.start().then(() => {
console.info('avRecorder start success');
}
```
5. 停止录像。
先通过avRecorder的stop方法停止录像,再通过videoOutput的stop方法停止录像输出流。
```ts
videoRecorder.stop().then(() => {
console.info('stop success');
}
videoOutput.stop((err) => {
if (err) {
console.error('Failed to stop the video output ${err.message}');
return;
}
console.info('Callback invoked to indicate the video output stop success.');
});
```
## 状态监听
在相机应用开发过程中,可以随时监听录像输出流状态,包括录像开始、录像结束、录像流输出的错误。
- 通过注册固定的frameStart回调函数获取监听录像开始结果,videoOutput创建成功时即可监听,录像第一次曝光时触发,有该事件返回结果则认为录像开始。
```ts
videoOutput.on('frameStart', () => {
console.info('Video frame started');
})
```
- 通过注册固定的frameEnd回调函数获取监听预览启动结果,videoOutput创建成功时即可监听,录像完成最后一帧时触发,有该事件返回结果则认为录像流已结束。
```ts
videoOutput.on('frameEnd', () => {
console.info('Video frame ended');
})
```
- 通过注册固定的error回调函数获取监听录像输出错误结果,callback返回预览输出接口使用错误时对应的错误码,错误码类型参见[CameraErrorCode](../reference/apis/js-apis-camera.md#cameraerrorcode)
```ts
videoOutput.on('error', (error) => {
console.info(`Video output error code: ${error.code}`);
})
```
# 会话管理
相机使用预览、拍照、录像、元数据功能前,均需要创建相机会话。
在会话中,可以完成以下功能:
- 配置相机的输入流和输出流。相机在拍摄前,必须完成输入输出流的配置。
配置输入流即添加设备输入,对用户而言,相当于选择设备的某一摄像头拍摄;配置输出流,即选择数据将以什么形式输出。当应用需要实现拍照时,输出流应配置为预览流和拍照流,预览流的数据将显示在XComponent组件上,拍照流的数据将通过ImageReceiver接口的能力保存到相册中。
- 添加闪光灯、调整焦距等配置。具体支持的配置及接口说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)
- 会话切换控制。应用可以通过移除和添加输出流的方式,切换相机模式。如当前会话的输出流为拍照流,应用可以将拍照流移除,然后添加视频流作为输出流,即完成了拍照到录像的切换。
完成会话配置后,应用提交和开启会话,可以开始调用相机相关功能。
## 开发步骤
1. 调用cameraManager类中的createCaptureSession()方法创建一个会话。
```ts
let captureSession;
try {
captureSession = cameraManager.createCaptureSession();
} catch (error) {
console.error('Failed to create the CaptureSession instance. errorCode = ' + error.code);
}
```
2. 调用captureSession类中的beginConfig()方法配置会话。
```ts
try {
captureSession.beginConfig();
} catch (error) {
console.error('Failed to beginConfig. errorCode = ' + error.code);
}
```
3. 使能。向会话中添加相机的输入流和输出流,调用captureSession.addInput()添加相机的输入流;调用captureSession.addOutput()添加相机的输出流。以下示例代码以添加预览流previewOutput和拍照流photoOutput为例,即当前模式支持拍照和预览。
调用captureSession类中的commitConfig()和start()方法提交相关配置,并启动会话。
```ts
try {
captureSession.addInput(cameraInput);
} catch (error) {
console.error('Failed to addInput. errorCode = ' + error.code);
}
try {
captureSession.addOutput(previewOutput);
} catch (error) {
console.error('Failed to addOutput(previewOutput). errorCode = ' + error.code);
}
try {
captureSession.addOutput(photoOutput);
} catch (error) {
console.error('Failed to addOutput(photoOutput). errorCode = ' + error.code);
}
await captureSession.commitConfig() ;
await captureSession.start().then(() => {
console.info('Promise returned to indicate the session start success.');
})
```
4. 会话控制。调用captureSession类中的stop()方法可以停止当前会话。调用removeOutput()和addOutput()方法可以完成会话切换控制。以下示例代码以移除拍照流photoOutput,添加视频流videoOutput为例,完成了拍照到录像的切换。
```ts
await captureSession.stop();
try {
captureSession.beginConfig();
} catch (error) {
console.error('Failed to beginConfig. errorCode = ' + error.code);
}
// 从会话中移除拍照输出流
try {
captureSession.removeOutput(photoOutput);
} catch (error) {
console.error('Failed to removeOutput(photoOutput). errorCode = ' + error.code);
}
// 向会话中添加视频输出流
try {
captureSession.addOutput(videoOutput);
} catch (error) {
console.error('Failed to addOutput(videoOutput). errorCode = ' + error.code);
}
```
# 相机开发指导
# 拍照实现方案
## 场景介绍
## 开发流程
OpenHarmony相机模块支持相机业务的开发,开发者可以通过已开放的接口实现相机硬件的访问、操作和新功能开发,最常见的操作如:预览、拍照和录像等。开发者也可以通过合适的接口或者接口组合实现闪光灯控制、曝光时间控制、手动对焦和自动对焦控制、变焦控制以及更多的功能
在获取到相机支持的输出流能力后,开始创建拍照流,开发流程如下
开发者在调用Camera能力时,需要了解Camera的一些基本概念:
![Photographing Development Process](figures/photographing-development-process.png)
- **相机静态能力**:用于描述相机的固有能力的一系列参数,比如朝向、支持的分辨率等信息。
- **物理相机**:物理相机就是独立的实体摄像头设备。物理相机ID是用于标志每个物理摄像头的唯一字串。
- **异步操作**:为保证UI线程不被阻塞,部分Camera接口采用异步调用方式。异步方式API均提供了callback函数和Promise函数。
## 完整示例
## 开发步骤
### 接口说明
详细API含义请参考:[相机管理API文档](../reference/apis/js-apis-camera.md)
### 全流程场景
包含流程:权限申请、创建实例、参数设置、会话管理、拍照、录像、释放资源等。
#### 权限申请
在使用相机之前,需要申请相机的相关权限,保证应用拥有相机硬件及其他功能权限,应用权限的介绍请参考权限章节,相机涉及权限如下表。
| 权限名称 | 权限属性值 |
| -------- | ------------------------------ |
| 相机权限 | ohos.permission.CAMERA |
| 录音权限 | ohos.permission.MICROPHONE |
| 存储权限 | ohos.permission.WRITE_MEDIA |
| 读取权限 | ohos.permission.READ_MEDIA |
| 位置权限 | ohos.permission.MEDIA_LOCATION |
参考代码如下:
```typescript
const PERMISSIONS: Array<string> = [
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE',
'ohos.permission.MEDIA_LOCATION',
'ohos.permission.READ_MEDIA',
'ohos.permission.WRITE_MEDIA'
]
function applyPermission() {
console.info('[permission] get permission');
globalThis.abilityContext.requestPermissionFromUser(PERMISSIONS)
}
```
#### 创建实例
在实现一个相机应用之前必须先创建一个独立的相机设备,然后才能继续相机的其他操作。如果此步骤操作失败,相机可能被占用或无法使用。如果被占用,必须等到相机释放后才能重新获取CameraManager对象。通过getSupportedCameras() 方法,获取当前使用的设备支持的相机列表。相机列表中存储了当前设备拥有的所有相机ID,如果列表不为空,则列表中的每个ID都支持独立创建相机对象;否则,说明正在使用的设备无可用的相机,不能继续后续的操作。相机设备具备预览、拍照、录像、Metadata等输出流,需要通过getSupportedOutputCapability()接口获取各个输出流的具体能力,通过该接口,可以获取当前设备支持的所有输出流能力,分别在CameraOutputCapability中的各个profile字段中,相机设备创建的建议步骤如下:
```typescript
```ts
import camera from '@ohos.multimedia.camera'
import image from '@ohos.multimedia.image'
import media from '@ohos.multimedia.media'
......@@ -67,8 +22,8 @@ if (!cameraManager) {
}
// 监听相机状态变化
cameraManager.on('cameraStatus', (cameraStatusInfo) => {
console.log(`camera : ${cameraStatusInfo.camera.cameraId}`);
console.log(`status: ${cameraStatusInfo.status}`);
console.info(`camera : ${cameraStatusInfo.camera.cameraId}`);
console.info(`status: ${cameraStatusInfo.status}`);
})
// 获取相机列表
......@@ -79,24 +34,24 @@ if (cameraArray.length <= 0) {
}
for (let index = 0; index < cameraArray.length; index++) {
console.log('cameraId : ' + cameraArray[index].cameraId); // 获取相机ID
console.log('cameraPosition : ' + cameraArray[index].cameraPosition); // 获取相机位置
console.log('cameraType : ' + cameraArray[index].cameraType); // 获取相机类型
console.log('connectionType : ' + cameraArray[index].connectionType); // 获取相机连接类型
console.info('cameraId : ' + cameraArray[index].cameraId); // 获取相机ID
console.info('cameraPosition : ' + cameraArray[index].cameraPosition); // 获取相机位置
console.info('cameraType : ' + cameraArray[index].cameraType); // 获取相机类型
console.info('connectionType : ' + cameraArray[index].connectionType); // 获取相机连接类型
}
// 创建相机输入流
let cameraInput
try {
cameraInput = cameraManager.createCameraInput(cameraArray[0]);
} catch () {
} catch (error) {
console.error('Failed to createCameraInput errorCode = ' + error.code);
}
// 监听cameraInput错误信息
let cameraDevice = cameraArray[0];
cameraInput.on('error', cameraDevice, (error) => {
console.log(`Camera input error code: ${error.code}`);
console.info(`Camera input error code: ${error.code}`);
})
// 打开相机
......@@ -120,17 +75,7 @@ if (!photoProfilesArray) {
console.error("createOutput photoProfilesArray == null || undefined")
}
let videoProfilesArray = cameraOutputCap.videoProfiles;
if (!videoProfilesArray) {
console.error("createOutput videoProfilesArray == null || undefined")
}
let metadataObjectTypesArray = cameraOutputCap.supportedMetadataObjectTypes;
if (!metadataObjectTypesArray) {
console.error("createOutput metadataObjectTypesArray == null || undefined")
}
// 创建预览输出流,其中参数 surfaceId 参考下面 XComponent 组件,预览流为XComponent组件提供的surface
// 创建预览输出流,其中参数 surfaceId 参考上文 XComponent 组件,预览流为XComponent组件提供的surface
let previewOutput
try {
previewOutput = cameraManager.createPreviewOutput(previewProfilesArray[0], surfaceId)
......@@ -140,7 +85,7 @@ try {
// 监听预览输出错误信息
previewOutput.on('error', (error) => {
console.log(`Preview output error code: ${error.code}`);
console.info(`Preview output error code: ${error.code}`);
})
// 创建ImageReceiver对象,并设置照片参数:分辨率大小是根据前面 photoProfilesArray 获取的当前设备所支持的拍照分辨率大小去设置
......@@ -154,130 +99,6 @@ try {
} catch (error) {
console.error('Failed to createPhotoOutput errorCode = ' + error.code);
}
// 创建视频录制的参数
let videoConfig = {
audioSourceType: 1,
videoSourceType: 1,
profile: {
audioBitrate: 48000,
audioChannels: 2,
audioCodec: 'audio/mp4v-es',
audioSampleRate: 48000,
durationTime: 1000,
fileFormat: 'mp4',
videoBitrate: 48000,
videoCodec: 'video/mp4v-es',
videoFrameWidth: 640,
videoFrameHeight: 480,
videoFrameRate: 30
},
url: 'file:///data/media/01.mp4',
orientationHint: 0,
maxSize: 100,
maxDuration: 500,
rotation: 0
}
// 创建录像输出流
let videoRecorder
media.createVideoRecorder().then((recorder) => {
console.log('createVideoRecorder called')
videoRecorder = recorder
})
// 设置视频录制的参数
videoRecorder.prepare(videoConfig)
//获取录像SurfaceId
let videoSurfaceId
videoRecorder.getInputSurface().then((id) => {
console.log('getInputSurface called')
videoSurfaceId = id
})
// 创建VideoOutput对象
let videoOutput
try {
videoOutput = cameraManager.createVideoOutput(videoProfilesArray[0], videoSurfaceId)
} catch (error) {
console.error('Failed to create the videoOutput instance. errorCode = ' + error.code);
}
// 监听视频输出错误信息
videoOutput.on('error', (error) => {
console.log(`Preview output error code: ${error.code}`);
})
```
预览流、拍照流和录像流的输入均需要提前创建surface,其中预览流为XComponent组件提供的surface,拍照流为ImageReceiver提供的surface,录像流为VideoRecorder的surface。
**XComponent**
```typescript
mXComponentController: XComponentController = new XComponentController // 创建XComponentController
build() {
Flex() {
XComponent({ // 创建XComponent
id: '',
type: 'surface',
libraryname: '',
controller: this.mXComponentController
})
.onload(() => { // 设置onload回调
// 设置Surface宽高(1920*1080),预览尺寸设置参考前面 previewProfilesArray 获取的当前设备所支持的预览分辨率大小去设置
this.mXComponentController.setXComponentSurfaceSize({surfaceWidth:1920,surfaceHeight:1080})
// 获取Surface ID
globalThis.surfaceId = mXComponentController.getXComponentSurfaceId()
})
.width('1920px') // 设置XComponent宽度
.height('1080px') // 设置XComponent高度
}
}
```
**ImageReceiver**
```typescript
function getImageReceiverSurfaceId() {
let receiver = image.createImageReceiver(640, 480, 4, 8)
console.log(TAG + 'before ImageReceiver check')
if (receiver !== undefined) {
console.log('ImageReceiver is ok')
surfaceId1 = receiver.getReceivingSurfaceId()
console.log('ImageReceived id: ' + JSON.stringify(surfaceId1))
} else {
console.log('ImageReceiver is not ok')
}
}
```
**VideoRecorder**
```typescript
function getVideoRecorderSurface() {
await getFd('CameraManager.mp4');
mVideoConfig.url = mFdPath;
media.createVideoRecorder((err, recorder) => {
console.info('Entering create video receiver')
mVideoRecorder = recorder
console.info('videoRecorder is :' + JSON.stringify(mVideoRecorder))
console.info('videoRecorder.prepare called.')
mVideoRecorder.prepare(mVideoConfig, (err) => {
console.info('videoRecorder.prepare success.')
mVideoRecorder.getInputSurface((err, id) => {
console.info('getInputSurface called')
mVideoSurface = id
console.info('getInputSurface surfaceId: ' + JSON.stringify(mVideoSurface))
})
})
})
}
```
#### 会话管理
##### 创建会话
```typescript
//创建会话
let captureSession
try {
......@@ -288,7 +109,7 @@ try {
// 监听session错误信息
captureSession.on('error', (error) => {
console.log(`Capture session error code: ${error.code}`);
console.info(`Capture session error code: ${error.code}`);
})
// 开始配置会话
......@@ -305,7 +126,7 @@ try {
console.error('Failed to addInput. errorCode = ' + error.code);
}
// 向会话中添加预览输
// 向会话中添加预览输
try {
captureSession.addOutput(previewOutput)
} catch (error) {
......@@ -324,49 +145,8 @@ await captureSession.commitConfig()
// 启动会话
await captureSession.start().then(() => {
console.log('Promise returned to indicate the session start success.');
})
```
##### 切换会话
```typescript
// 停止当前会话
await captureSession.stop()
// 开始配置会话
try {
captureSession.beginConfig()
} catch (error) {
console.error('Failed to beginConfig. errorCode = ' + error.code);
}
// 从会话中移除拍照输出流
try {
captureSession.removeOutput(photoOutput)
} catch (error) {
console.error('Failed to removeOutput(photoOutput). errorCode = ' + error.code);
}
// 向会话中添加录像输出流
try {
captureSession.addOutput(videoOutput)
} catch (error) {
console.error('Failed to addOutput(videoOutput). errorCode = ' + error.code);
}
// 提交会话配置
await captureSession.commitConfig()
// 启动会话
await captureSession.start().then(() => {
console.log('Promise returned to indicate the session start success.');
console.info('Promise returned to indicate the session start success.');
})
```
#### 参数设置
```typescript
// 判断设备是否支持闪光灯
let flashStatus
try {
......@@ -374,7 +154,7 @@ try {
} catch (error) {
console.error('Failed to hasFlash. errorCode = ' + error.code);
}
console.log('Promise returned with the flash light support status:' + flashStatus);
console.info('Promise returned with the flash light support status:' + flashStatus);
if (flashStatus) {
// 判断是否支持自动闪光灯模式
......@@ -427,11 +207,6 @@ try {
} catch (error) {
console.error('Failed to set the zoom ratio value. errorCode = ' + error.code);
}
```
#### 拍照
```typescript
let settings = {
quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, // 设置图片质量高
rotation: camera.ImageRotation.ROTATION_0 // 设置图片旋转角度0
......@@ -442,47 +217,8 @@ photoOutput.capture(settings, async (err) => {
console.error('Failed to capture the photo ${err.message}');
return;
}
console.log('Callback invoked to indicate the photo capture request success.');
});
```
#### 录像
```typescript
// 启动录像输出流
videoOutput.start(async (err) => {
if (err) {
console.error('Failed to start the video output ${err.message}');
return;
}
console.log('Callback invoked to indicate the video output start success.');
});
// 开始录像
videoRecorder.start().then(() => {
console.info('videoRecorder start success');
}
// 停止录像
videoRecorder.stop().then(() => {
console.info('stop success');
}
// 停止录像输出流
videoOutput.stop((err) => {
if (err) {
console.error('Failed to stop the video output ${err.message}');
return;
}
console.log('Callback invoked to indicate the video output stop success.');
console.info('Callback invoked to indicate the photo capture request success.');
});
```
拍照保存接口可参考:[图片处理API文档](image.md#imagereceiver的使用)
#### 释放资源
```typescript
// 停止当前会话
captureSession.stop()
......@@ -495,9 +231,6 @@ previewOutput.release()
// 释放拍照输出流
photoOutput.release()
// 释放录像输出流
videoOutput.release()
// 释放会话
captureSession.release()
......@@ -505,7 +238,3 @@ captureSession.release()
captureSession = null
```
## 流程图
应用使用相机的流程示意图如下
![camera_framework process](figures/camera_framework_process.png)
\ No newline at end of file
# 拍照
拍照是相机的最重要功能之一,拍照模块基于相机复杂的逻辑,为了保证用户拍出的照片质量,在中间步骤可以设置分辨率、闪光灯、焦距、照片质量及旋转角度等信息。
## 开发步骤
详细的API说明请参考[Camera API参考](../reference/apis/js-apis-camera.md)
1. 创建拍照输出流的SurfaceId以及拍照输出的数据,都需要用到系统提供的image接口能力,导入image接口的方法如下。
```ts
import image from '@ohos.multimedia.image';
```
2. 获取SurfaceId。
通过image的createImageReceiver方法创建ImageReceiver实例,再通过实例的getReceivingSurfaceId方法获取SurfaceId,与拍照输出流相关联,获取拍照输出流的数据。
```ts
function getImageReceiverSurfaceId() {
let receiver = image.createImageReceiver(640, 480, 4, 8);
console.info('before ImageReceiver check');
if (receiver !== undefined) {
console.info('ImageReceiver is ok');
let photoSurfaceId = receiver.getReceivingSurfaceId();
console.info('ImageReceived id: ' + JSON.stringify(photoSurfaceId));
} else {
console.info('ImageReceiver is not ok');
}
}
```
3. 创建拍照输出流。
通过CameraOutputCapability类中的photoProfiles()方法,可获取当前设备支持的拍照输出流,通过createPhotoOutput()方法传入支持的某一个输出流及步骤一获取的SurfaceId创建拍照输出流。
```ts
let photoProfilesArray = cameraOutputCapability.photoProfiles;
if (!photoProfilesArray) {
console.error("createOutput photoProfilesArray == null || undefined");
}
let photoOutput;
try {
photoOutput = cameraManager.createPhotoOutput(photoProfilesArray[0], photoSurfaceId);
} catch (error) {
console.error('Failed to createPhotoOutput errorCode = ' + error.code);
}
```
4. 参数配置。
配置相机的参数可以调整拍照的一些功能,包括闪光灯、变焦、焦距等。
```ts
// 判断设备是否支持闪光灯
let flashStatus;
try {
flashStatus = captureSession.hasFlash();
} catch (error) {
console.error('Failed to hasFlash. errorCode = ' + error.code);
}
console.info('Promise returned with the flash light support status:' + flashStatus);
if (flashStatus) {
// 判断是否支持自动闪光灯模式
let flashModeStatus;
try {
let status = captureSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_AUTO);
flashModeStatus = status;
} catch (error) {
console.error('Failed to check whether the flash mode is supported. errorCode = ' + error.code);
}
if(flashModeStatus) {
// 设置自动闪光灯模式
try {
captureSession.setFlashMode(camera.FlashMode.FLASH_MODE_AUTO);
} catch (error) {
console.error('Failed to set the flash mode. errorCode = ' + error.code);
}
}
}
// 判断是否支持连续自动变焦模式
let focusModeStatus;
try {
let status = captureSession.isFocusModeSupported(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO);
focusModeStatus = status;
} catch (error) {
console.error('Failed to check whether the focus mode is supported. errorCode = ' + error.code);
}
if (focusModeStatus) {
// 设置连续自动变焦模式
try {
captureSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO);
} catch (error) {
console.error('Failed to set the focus mode. errorCode = ' + error.code);
}
}
// 获取相机支持的可变焦距比范围
let zoomRatioRange;
try {
zoomRatioRange = captureSession.getZoomRatioRange();
} catch (error) {
console.error('Failed to get the zoom ratio range. errorCode = ' + error.code);
}
// 设置可变焦距比
try {
captureSession.setZoomRatio(zoomRatioRange[0]);
} catch (error) {
console.error('Failed to set the zoom ratio value. errorCode = ' + error.code);
}
```
5. 触发拍照。
通过photoOutput类的capture()方法,执行拍照任务。该方法有两个参数,第一个参数为拍照设置参数的setting,setting中可以设置照片的质量和旋转角度,第二参数为回调函数。
```ts
let settings = {
quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, // 设置图片质量高
rotation: camera.ImageRotation.ROTATION_0, // 设置图片旋转角度0
location: captureLocation, // 设置图片地理位置
mirror: false // 设置镜像使能开关(默认关)
};
photoOutput.capture(settings, async (err) => {
if (err) {
console.error('Failed to capture the photo ${err.message}');
return;
}
console.info('Callback invoked to indicate the photo capture request success.');
});
```
## 状态监听
在相机应用开发过程中,可以随时监听拍照输出流状态,包括拍照流开始、拍照帧的开始与结束、拍照输出流的错误。
- 通过注册固定的captureStart回调函数获取监听拍照开始结果,photoOutput时即可监听,拍照第一次曝光时触发,该事件返回此次拍照的captureId。
```ts
photoOutput.on('captureStart', (captureId) => {
console.info(`photo capture stated, captureId : ${captureId}`);
})
```
- 通过注册固定的frameShutter回调函数获取监听拍照结束结果,photoOutput时即可监听,该事件返回结果为拍照完全结束后的相关信息[CaptureEndInfo](../reference/apis/js-apis-camera.md#captureendinfo)
```ts
photoOutput.on('captureEnd', (captureEndInfo) => {
console.info(`photo capture end, captureId : ${captureEndInfo.captureId}`);
console.info(`frameCount : ${captureEndInfo.frameCount}`);
})
```
- 通过注册固定的error回调函数获取监听拍照输出流的错误结果。callback返回拍照输出接口使用错误时的对应错误码,错误码类型参见[CameraErrorCode](../reference/apis/js-apis-camera.md#cameraerrorcode)
```ts
photoOutput.on('error', (error) => {
console.info(`Photo output error code: ${error.code}`);
})
```
# 分布式音频播放(仅对系统应用开放)
通过分布式音频播放的能力,用户可以将音频投播远端设备播放,实现音频在组网中不同设备之间流转。
开发者可以通过分布式音频播放,将当前设备播放的所有音频投放到指定的远端设备播放,或将设备播放的某个音频流投放到指定的远端设备播放。
## 开发步骤及示例
在将音频投播到组网内其他设备前,需要先获取组网内的设备列表,并监听设备连接状态的变化,具体开发步骤请参考[音频输出设备管理](audio-output-device-management.md)
在获取组网内的设备列表时,可以通过指定DeviceFlag,筛选出需要的设备。
| 名称 | 说明 |
| -------- | -------- |
| NONE_DEVICES_FLAG<sup>9+</sup> | 无。此接口为系统接口。 |
| OUTPUT_DEVICES_FLAG | 本地输出设备。 |
| INPUT_DEVICES_FLAG | 本地输入设备。 |
| ALL_DEVICES_FLAG | 本地输入输出设备。 |
| DISTRIBUTED_OUTPUT_DEVICES_FLAG<sup>9+</sup> | 分布式输出设备。&nbsp;此接口为系统接口。 |
| DISTRIBUTED_INPUT_DEVICES_FLAG<sup>9+</sup> | 分布式输入设备。&nbsp;此接口为系统接口。 |
| ALL_DISTRIBUTED_DEVICES_FLAG<sup>9+</sup> | 分布式输入输出设备。&nbsp;此接口为系统接口。 |
具体接口说明请参考[AudioRoutingManager API文档](../reference/apis/js-apis-audio.md#audioroutingmanager9)
### 投播所有音频
1. [获取输出设备信息](audio-output-device-management.md#获取输出设备信息)
2. 创建AudioDeviceDescriptor对象,用于指定音频输出设备。
3. 调用selectOutputDevice,将当前设备播放的所有音频投放到指定的远端设备播放。
```ts
let outputAudioDeviceDescriptor = [{
deviceRole: audio.DeviceRole.OUTPUT_DEVICE,
deviceType: audio.DeviceType.SPEAKER,
id: 1,
name: "",
address: "",
sampleRates: [44100],
channelCounts: [2],
channelMasks: [0],
networkId: audio.LOCAL_NETWORK_ID,
interruptGroupId: 1,
volumeGroupId: 1,
}];
async function selectOutputDevice() {
audioRoutingManager.selectOutputDevice(outputAudioDeviceDescriptor, (err) => {
if (err) {
console.error(`Invoke selectOutputDevice failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info('Invoke selectOutputDevice succeeded.');
}
});
}
```
### 投播指定音频流
1. [获取输出设备信息](audio-output-device-management.md#获取输出设备信息)
2. 创建AudioRendererFilter对象,通过uid指定应用,通过rendererId指定音频流。
3. 创建AudioDeviceDescriptor对象,用于指定音频输出设备。
4. 调用selectOutputDeviceByFilter,将当前设备播放的指定音频流投放到指定的远端设备播放。
```ts
let outputAudioRendererFilter = {
uid: 20010041,
rendererInfo: {
content: audio.ContentType.CONTENT_TYPE_MUSIC,
usage: audio.StreamUsage.STREAM_USAGE_MEDIA,
rendererFlags: 0 },
rendererId: 0 };
let outputAudioDeviceDescriptor = [{
deviceRole: audio.DeviceRole.OUTPUT_DEVICE,
deviceType: audio.DeviceType.SPEAKER,
id: 1,
name: "",
address: "",
sampleRates: [44100],
channelCounts: [2],
channelMasks: [0],
networkId: audio.LOCAL_NETWORK_ID,
interruptGroupId: 1,
volumeGroupId: 1,
}];
async function selectOutputDeviceByFilter() {
audioRoutingManager.selectOutputDeviceByFilter(outputAudioRendererFilter, outputAudioDeviceDescriptor, (err) => {
if (err) {
console.error(`Invoke selectOutputDeviceByFilter failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info('Invoke selectOutputDeviceByFilter succeeded.');
}
});
}
```
# 分布式媒体会话概述
OpenHarmony提供的媒体会话允许用户把本地播放的媒体投播到远端分布式设备上,从而实现更好的播放效果,例如将平板上播放的音频投播到智能音箱。
当用户将音频媒体投播到远端设备后,可以实时将媒体信息同步到远端设备,同时远端被投播的设备也可以反向控制源端音源,进行上一首/下一首/播放/暂停等控制,用户层面上整个播放控制流程和本地播放一致。
## 交互过程
当本地投播主控端设备与远端被控端设备成功配对之后,主控端设备中的媒体会话控制方,可以通过媒体会话管理器将媒体投播到被控设备上,实现分布式媒体会话,交互过程如图所示。
![Distributed AVSession Interaction Process](figures/distributed-avsession-interaction-process.png)
媒体投播时,被控端设备上的AVSession服务会自动创建一个与主控设备保持同步的媒体会话,主控设备的会话信息、主控设备的命令事件与被控设备上的命令事件时刻保持同步。
## 分布式媒体会话流程
用户进行了分布式投播以后,在投播被控端自动创建出对应的媒体会话,两侧的媒体会话可以相互联动,具体体现在:
1. 主控端本地媒体会话服务接收到音频跨设备切换命令后将本端的媒体会话信息同步到远端。
2. 远端播控中心监听到新增会话后,创建对应会话的控制器。
3. 远端播控中心可以通过远端的控制器将控制命令发送到主控端本地媒体会话。
4. 主控端本地媒体会话接收远端控制命令后,回调到本端的音频类应用。
5. 主控端本地的媒体会话信息变更实时同步到远端播控中心显示。
6. 远端设备连接断开后,音频切回主控端本地并暂停播放(音频部件完成回切,媒体会话通知应用暂停)。
## 分布式媒体会话场景
应用通过分布式媒体会话进行投播时,存在两种场景:
- 系统投播:通过媒体会话控制方,例如播控中心,发起分布式投播。
系统投播对所有应用生效。系统投播后,主控端设备中的所有音频默认从被控端设备播放。
- 应用投播:音视频应用在自身应用内通过集成投播组件发起分布式投播(当前暂未支持)。
应用投播对单个应用生效。应用投播后,主控端设备中发起投播的应用音频从被控端设备播放,其它应用的音频依旧从主控端设备播放。
另外,投播支持抢占,后投播应用可以抢占之前投播的应用,在远端设备进行音频播放。
## 分布式媒体会话与分布式音频播放之间的关系
媒体会话服务在实现分布式媒体会话进行跨设备投播时,内部的实现逻辑可以粗略描述为:
- 调用了[分布式音频播放](distributed-audio-playback.md)能力的相关接口,将音频流投播到了远端设备。
- 通过分布式能力将媒体会话元数据投播到了远端设备,供远端设备获取并显示。
所以,通过分布式媒体会话进行投播,不仅可以实现在远端设备播放音频,而且可以让远端设备显示播放信息,同时借助媒体会话的机制,可以实现在远端设备上对播放的音频进行控制。
# 图片解码
图片解码指将所支持格式的存档图片解码成统一的[PixelMap](image-overview.md),以便在应用或系统中进行图片显示或[图片处理](image-overview.md)。当前支持的存档图片格式包括JPEG、PNG、GIF、RAW、WebP、BMP、SVG。
## 开发步骤
图片解码相关API的详细介绍请参见:[图片解码接口说明](../reference/apis/js-apis-image.md#imagesource)
1. 全局导入Image模块。
```ts
import image from '@ohos.multimedia.image';
```
2. 获取图片。
- 方法一:获取沙箱路径。具体请参考[获取应用文件路径](../application-models/application-context-stage.md#获取应用开发路径)。应用沙箱的介绍及如何向应用沙箱推送文件,请参考[文件管理](../file-management/app-sandbox-directory.md)
```ts
// Stage模型参考如下代码
const context = getContext(this);
const filePath = context.cacheDir + '/test.jpg';
```
```ts
// FA模型参考如下代码
import featureAbility from '@ohos.ability.featureAbility';
const context = featureAbility.getContext();
const filePath = context.getCacheDir() + "/test.jpg";
```
- 方法二:通过沙箱路径获取图片的文件描述符。具体请参考[file.fs API参考文档](../reference/apis/js-apis-file-fs.md)
该方法需要先导入\@ohos.file.fs模块。
```ts
import fs from '@ohos.file.fs';
```
然后调用fs.openSync()获取文件描述符。
```ts
// Stage模型参考如下代码
const context = getContext(this);
const filePath = context.cacheDir + '/test.jpg';
const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
const fd = file?.fd;
```
```ts
// FA模型参考如下代码
import featureAbility from '@ohos.ability.featureAbility';
const context = featureAbility.getContext();
const filePath = context.getCacheDir() + "/test.jpg";
const file = fs.openSync(filePath, fs.OpenMode.READ_WRITE);
const fd = file?.fd;
```
- 方法三:通过资源管理器获取资源文件的ArrayBuffer。具体请参考[ResourceManager API参考文档](../reference/apis/js-apis-resource-manager.md#getrawfilecontent9-1)
```ts
// Stage模型
const context = getContext(this);
// 获取resourceManager资源管理器
const resourceMgr = context.resourceManager;
```
```ts
// FA模型
// 导入resourceManager资源管理器
import resourceManager from '@ohos.resourceManager';
const resourceMgr = await resourceManager.getResourceManager();
```
不同模型获取资源管理器的方式不同,获取资源管理器后,再调用resourceMgr.getRawFileContent()获取资源文件的ArrayBuffer。
```ts
const fileData = await resourceMgr.getRawFileContent('test.jpg');
// 获取图片的ArrayBuffer
const buffer = fileData.buffer;
```
3. 创建ImageSource实例。
- 方法一:通过沙箱路径创建ImageSource。沙箱路径可以通过步骤2的方法一获取。
```ts
// path为已获得的沙箱路径
const imageSource = image.createImageSource(filePath);
```
- 方法二:通过文件描述符fd创建ImageSource。文件描述符可以通过步骤2的方法二获取。
```ts
// fd为已获得的文件描述符
const imageSource = image.createImageSource(fd);
```
- 方法三:通过缓冲区数组创建ImageSource。缓冲区数组可以通过步骤2的方法三获取。
```ts
const imageSource = image.createImageSource(buffer);
```
4. 设置解码参数DecodingOptions,解码获取PixelMap图片对象。
```ts
let decodingOptions = {
editable: true,
desiredPixelFormat: 3,
}
// 创建pixelMap并进行简单的旋转和缩放
const pixelMap = await imageSource.createPixelMap(decodingOptions);
```
解码完成,获取到PixelMap对象后,可以进行后续[图片处理](image-processing.md)
## 开发示例-对资源文件中的图片进行解码
1. 获取resourceManager资源管理。
```ts
const context = getContext(this);
// 获取resourceManager资源管理
const resourceMgr = context.resourceManager;
```
2. 获取rawfile文件夹下test.jpg的ArrayBuffer。
```ts
const fileData = await resourceMgr.getRawFileContent('test.jpg');
// 获取图片的ArrayBuffer
const buffer = fileData.buffer;
```
3. 创建imageSource。
```ts
const imageSource = image.createImageSource(buffer);
```
4. 创建PixelMap。
```ts
const pixelMap = await imageSource.createPixelMap();
```
# 图片编码
图片编码指将PixelMap编码成不同格式的存档图片(当前仅支持打包为JPEG和WebP格式),用于后续处理,如保存、传输等。
## 开发步骤
图片编码相关API的详细介绍请参见:[图片编码接口说明](../reference/apis/js-apis-image.md#imagepacker)
1. 创建图像编码ImagePacker对象。
```ts
// 导入相关模块包
import image from '@ohos.multimedia.image';
const imagePackerApi = image.createImagePacker();
```
2. 设置编码输出流和编码参数。
format为图像的编码格式;quality为图像质量,范围从0-100,100为最佳质量。
```ts
let packOpts = { format:"image/jpeg", quality:98 };
```
3. [创建PixelMap对象或创建ImageSource](image-decoding.md)对象。
4. 进行图片编码,并保存编码后的图片。
方法一:通过PixelMap进行编码。
```ts
imagePackerApi.packing(pixelMap, packOpts).then( data => {
// data 为打包获取到的文件流,写入文件保存即可得到一张图片
}).catch(error => {
console.error('Failed to pack the image. And the error is: ' + error);
})
```
方法二:通过imageSource进行编码。
```ts
imagePackerApi.packing(imageSource, packOpts).then( data => {
// data 为打包获取到的文件流,写入文件保存即可得到一张图片
}).catch(error => {
console.error('Failed to pack the image. And the error is: ' + error);
})
```
# 图片开发概述
应用开发中的图片开发是对图片像素数据进行解析、处理、构造的过程,达到目标图片效果,主要涉及图片解码、图片处理、图片编码等。
在学习图片开发前,需要熟悉以下基本概念。
- 图片解码
指将所支持格式的存档图片解码成统一的PixelMap,以便在应用或系统中进行图片显示或图片处理。当前支持的存档图片格式包括JPEG、PNG、GIF、RAW、WebP、BMP、SVG。
- PixelMap
指图片解码后无压缩的位图,用于图片显示或图片处理。
- 图片处理
指对PixelMap进行相关的操作,如旋转、缩放、设置透明度、获取图片信息、读写像素数据等。
- 图片编码
指将PixelMap编码成不同格式的存档图片(当前仅支持JPEG和WebP),用于后续处理,如保存、传输等。
图片开发的主要流程如下图所示。
**图1** 图片开发流程示意图  
![Image development process](figures/image-development-process.png)
1. 获取图片:通过应用沙箱等方式获取原始图片。
2. 创建ImageSource实例:ImageSource是图片解码出来的图片源类,用于获取或修改图片相关信息。
3. [图片解码](image-decoding.md):通过ImageSource解码生成PixelMap。
4. [图片处理](image-transformation.md):对PixelMap进行处理,更改图片属性实现图片的旋转、缩放、裁剪等效果。然后通过[Image组件](../ui/arkts-graphics-display.md)显示图片。
5. [图片编码](image-encoding.md):使用图片打包器类ImagePacker,将PixelMap或ImageSource进行压缩编码,生成一张新的图片。
除上述基本图片开发能力外,OpenHarmony还提供常用[图片工具](image-tool.md),供开发者选择使用。
# 位图操作
当需要对目标图片中的部分区域进行处理时,可以使用位图操作功能。此功能常用于图片美化等操作。
如下图所示,一张图片中,将指定的矩形区域像素数据读取出来,进行修改后,再写回原图片对应区域。
**图1** 位图操作示意图  
![Bitmap operation](figures/bitmap-operation.png)
## 开发步骤
位图操作相关API的详细介绍请参见[API参考](../reference/apis/js-apis-image.md#pixelmap7)
1. 完成[图片解码](image-decoding.md#开发步骤),获取PixelMap位图对象。
2. 从PixelMap位图对象中获取信息。
```ts
// 获取图像像素的总字节数
let pixelBytesNumber = pixelMap.getPixelBytesNumber();
// 获取图像像素每行字节数
let rowCount = pixelMap.getBytesNumberPerRow();
// 获取当前图像像素密度。像素密度是指每英寸图片所拥有的像素数量。像素密度越大,图片越精细。
let getDensity = pixelMap.getDensity();
```
3. 读取并修改目标区域像素数据,写回原图。
```ts
// 场景一:将读取的整张图像像素数据结果写入ArrayBuffer中
const readBuffer = new ArrayBuffer(pixelBytesNumber);
pixelMap.readPixelsToBuffer(readBuffer).then(() => {
console.info('Succeeded in reading image pixel data.');
}).catch(error => {
console.error('Failed to read image pixel data. And the error is: ' + error);
})
// 场景二:读取指定区域内的图片数据,结果写入area.pixels中
const area = {
pixels: new ArrayBuffer(8),
offset: 0,
stride: 8,
region: { size: { height: 1, width: 2 }, x: 0, y: 0 }
}
pixelMap.readPixels(area).then(() => {
console.info('Succeeded in reading the image data in the area.');
}).catch(error => {
console.error('Failed to read the image data in the area. And the error is: ' + error);
})
// 对于读取的图片数据,可以独立使用(创建新的pixelMap),也可以对area.pixels进行所需修改
// 将图片数据area.pixels写入指定区域内
pixelMap.writePixels(area).then(() => {
console.info('Succeeded to write pixelMap into the specified area.');
})
// 将图片数据结果写入pixelMap中
const writeColor = new ArrayBuffer(96);
pixelMap.writeBufferToPixels(writeColor, () => {});
```
# 图片工具
图片工具当前主要提供图片EXIF信息的读取与编辑能力。
EXIF(Exchangeable image file format)是专门为数码相机的照片设定的文件格式,可以记录数码照片的属性信息和拍摄数据。当前仅支持JPEG格式图片。
在图库等应用中,需要查看或修改数码照片的EXIF信息。由于摄像机的手动镜头的参数无法自动写入到EXIF信息中或者因为相机断电等原因经常会导致拍摄时间出错,这时候就需要手动修改错误的EXIF数据,即可使用本功能。
OpenHarmony目前仅支持对部分EXIF信息的查看和修改,具体支持的范围请参见:[EIXF信息](../reference/apis/js-apis-image.md#propertykey7)
## 开发步骤
EXIF信息的读取与编辑相关API的详细介绍请参见[API参考](../reference/apis/js-apis-image.md#getimageproperty7)
1. 获取图片,创建图片源ImageSource。
```ts
// 导入相关模块包
import image from '@ohos.multimedia.image';
// 获取沙箱路径创建ImageSource
const fd = ...; // 获取需要被处理的图片的fd
const imageSource = image.createImageSource(fd);
```
2. 读取、编辑EXIF信息。
```ts
// 读取EXIF信息,BitsPerSample为每个像素比特数
imageSource.getImageProperty('BitsPerSample', (error, data) => {
if (error) {
console.error('Failed to get the value of the specified attribute key of the image.And the error is: ' + error);
} else {
console.info('Succeeded in getting the value of the specified attribute key of the image ' + data);
}
})
// 编辑EXIF信息
imageSource.modifyImageProperty('ImageWidth', '120').then(() => {
const width = imageSource.getImageProperty("ImageWidth");
console.info('The new imageWidth is ' + width);
})
```
# 图像变换
图片处理指对PixelMap进行相关的操作,如获取图片信息、裁剪、缩放、偏移、旋转、翻转、设置透明度、读写像素数据等。图片处理主要包括图像变换、[位图操作](image-pixelmap-operation.md),本文介绍图像变换。
## 开发步骤
图像变换相关API的详细介绍请参见[API参考](../reference/apis/js-apis-image.md#pixelmap7)
1. 完成[图片解码](image-decoding.md#开发步骤),获取Pixelmap对象。
2. 获取图片信息。
```
// 获取图片大小
pixelMap.getImageInfo().then( info => {
console.info('info.width = ' + info.size.width);
console.info('info.height = ' + info.size.height);
}).catch((err) => {
console.error("Failed to obtain the image pixel map information.And the error is: " + err);
});
```
3. 进行图像变换操作。
原图:
![Original drawing](figures/original-drawing.jpeg)
- 裁剪
```
// x:裁剪起始点横坐标0
// y:裁剪起始点纵坐标0
// height:裁剪高度400,方向为从上往下
// width:裁剪宽度400,方向为从左到右
pixelMap.crop({x: 0, y: 0, size: { height: 400, width: 400 } });
```
![cropping](figures/cropping.jpeg)
- 缩放
```
// 宽为原来的0.5
// 高为原来的0.5
pixelMap.scale(0.5, 0.5);
```
![zoom](figures/zoom.jpeg)
- 偏移
```
// 向下偏移100
// 向右偏移100
pixelMap.translate(100, 100);
```
![offsets](figures/offsets.jpeg)
- 旋转
```
// 顺时针旋转90°
pixelMap.rotate(90);
```
![rotate](figures/rotate.jpeg)
- 翻转
```
// 垂直翻转
pixelMap.flip(false, true);
```
![Vertical Flip](figures/vertical-flip.jpeg)
```
// 水平翻转
pixelMap.flip(true, false);
```
![Horizontal Flip](figures/horizontal-flip.jpeg)
- 透明度
```
// 透明度0.5
pixelMap.opacity(0.5);
```
![Transparency](figures/transparency.png)
# 图片开发指导
## 场景介绍
图片开发的主要工作是将获取到的图片进行解码,将解码后的pixelmap编码成支持的格式,本文将对图片的解码、编码等场景开发进行介绍说明。
## 接口说明
详细API含义请参考:[图片处理API文档](../reference/apis/js-apis-image.md)
## 开发步骤
### 全流程场景
包含流程:创建实例、读取图片信息、读写pixelmap、更新数据、打包像素、释放资源等流程。
```js
const color = new ArrayBuffer(96); // 用于存放图像像素数据
let opts = { alphaType: 0, editable: true, pixelFormat: 4, scaleMode: 1, size: { height: 2, width: 3 } } // 图像像素数据
// 创建pixelmap对象
image.createPixelMap(color, opts, (err, pixelmap) => {
console.log('Succeeded in creating pixelmap.');
// 创建pixelmap对象失败
if (err) {
console.info('create pixelmap failed, err' + err);
return
}
// 用于读像素
const area = {
pixels: new ArrayBuffer(8),
offset: 0,
stride: 8,
region: { size: { height: 1, width: 2 }, x: 0, y: 0 }
}
pixelmap.readPixels(area,() => {
let bufferArr = new Uint8Array(area.pixels);
let res = true;
for (let i = 0; i < bufferArr.length; i++) {
console.info(' buffer ' + bufferArr[i]);
if(res) {
if(bufferArr[i] == 0) {
res = false;
console.log('readPixels end.');
break;
}
}
}
})
// 用于存像素
const readBuffer = new ArrayBuffer(96);
pixelmap.readPixelsToBuffer(readBuffer,() => {
let bufferArr = new Uint8Array(readBuffer);
let res = true;
for (let i = 0; i < bufferArr.length; i++) {
if(res) {
if (bufferArr[i] !== 0) {
res = false;
console.log('readPixelsToBuffer end.');
break;
}
}
}
})
// 用于写像素
pixelmap.writePixels(area,() => {
const readArea = { pixels: new ArrayBuffer(20), offset: 0, stride: 8, region: { size: { height: 1, width: 2 }, x: 0, y: 0 }}
pixelmap.readPixels(readArea,() => {
let readArr = new Uint8Array(readArea.pixels);
let res = true;
for (let i = 0; i < readArr.length; i++) {
if(res) {
if (readArr[i] !== 0) {
res = false;
console.log('readPixels end.please check buffer');
break;
}
}
}
})
})
const writeColor = new ArrayBuffer(96); //图像像素数据
// 用于写像素到缓冲区
pixelmap.writeBufferToPixels(writeColor).then(() => {
const readBuffer = new ArrayBuffer(96);
pixelmap.readPixelsToBuffer(readBuffer).then (() => {
let bufferArr = new Uint8Array(readBuffer);
let res = true;
for (let i = 0; i < bufferArr.length; i++) {
if(res) {
if (bufferArr[i] !== i) {
res = false;
console.log('readPixels end.please check buffer');
break;
}
}
}
})
})
// 用于获取图片信息
pixelmap.getImageInfo((err, imageInfo) => {
// 获取图片信息失败
if (err || imageInfo == null) {
console.info('getImageInfo failed, err' + err);
return
}
if (imageInfo !== null) {
console.log('Succeeded in getting imageInfo');
}
})
// 用于释放pixelmap
pixelmap.release(()=>{
console.log('Succeeded in releasing pixelmap');
})
})
// 用于创建imagesource(uri)
let path = '/data/local/tmp/test.jpg';
const imageSourceApi1 = image.createImageSource(path);
// 用于创建imagesource(fd)
let fd = 29;
const imageSourceApi2 = image.createImageSource(fd);
// 用于创建imagesource(data)
const data = new ArrayBuffer(96);
const imageSourceApi3 = image.createImageSource(data);
// 用于释放imagesource
imageSourceApi3.release(() => {
console.log('Succeeded in releasing imagesource');
})
// 用于编码
const imagePackerApi = image.createImagePacker();
const imageSourceApi = image.createImageSource(0);
let packOpts = { format:"image/jpeg", quality:98 };
imagePackerApi.packing(imageSourceApi, packOpts, (err, data) => {
if (err) {
console.info('packing from imagePackerApi failed, err' + err);
return
}
console.log('Succeeded in packing');
})
// 用于释放imagepacker
imagePackerApi.release();
```
### 解码场景
```js
let path = '/data/local/tmp/test.jpg'; // 设置创建imagesource的路径
// 用路径创建imagesource
const imageSourceApi = image.createImageSource(path); // '/data/local/tmp/test.jpg'
// 设置参数
let decodingOptions = {
sampleSize:1, // 缩略图采样大小
editable: true, // 是否可编辑
desiredSize:{ width:1, height:2}, // 期望输出大小
rotateDegrees:10, // 旋转角度
desiredPixelFormat:2, // 解码的像素格式
desiredRegion: { size: { height: 1, width: 2 }, x: 0, y: 0 }, // 解码的区域
index:0 // 图片序号
};
// 用于回调方式创建pixelmap
imageSourceApi.createPixelMap(decodingOptions, (err, pixelmap) => {
// 创建pixelmap对象失败
if (err) {
console.info('create pixelmap failed, err' + err);
return
}
console.log('Succeeded in creating pixelmap.');
})
// 用于promise创建pixelmap
imageSourceApi.createPixelMap().then(pixelmap => {
console.log('Succeeded in creating pixelmap.');
// 用于获取像素每行字节数
let num = pixelmap.getBytesNumberPerRow();
// 用于获取像素总字节数
let pixelSize = pixelmap.getPixelBytesNumber();
// 用于获取pixelmap信息
pixelmap.getImageInfo().then( imageInfo => {});
// 用于释放pixelmap
pixelmap.release(()=>{
console.log('Succeeded in releasing pixelmap');
})
}).catch(error => {
console.log('Failed in creating pixelmap.' + error);
})
```
### 编码场景
```js
let path = '/data/local/tmp/test.png'; // 设置创建imagesource的路径
// 用于设置imagesource
const imageSourceApi = image.createImageSource(path); // '/data/local/tmp/test.png'
// 如果创建imagesource失败,打印错误信息
if (imageSourceApi == null) {
console.log('Failed in creating imageSource.');
}
// 如果创建imagesource成功,则创建imagepacker
const imagePackerApi = image.createImagePacker();
// 如果创建失败,打印错误信息
if (imagePackerApi == null) {
console.log('Failed in creating imagePacker.');
}
// 如果创建imagepacker成功,则设置编码参数
let packOpts = { format:"image/jpeg", // 支持编码的格式为jpg
quality:98 } // 图片质量0-100
// 用于编码
imagePackerApi.packing(imageSourceApi, packOpts)
.then( data => {
console.log('Succeeded in packing');
})
// 编码完成,释放imagepacker
imagePackerApi.release();
// 用于获取imagesource信息
imageSourceApi.getImageInfo((err, imageInfo) => {
console.log('Succeeded in getting imageInfo');
})
const array = new ArrayBuffer(100); //增量数据
// 用于更新增量数据
imageSourceApi.updateData(array, false, 0, 10,(error, data)=> {})
```
### ImageReceiver的使用
示例场景:camera作为客户端将拍照数据传给服务端
```js
public async init(surfaceId: any) {
// 服务端代码,创建ImageReceiver
let receiver = image.createImageReceiver(8 * 1024, 8, image.ImageFormat.JPEG, 1);
// 获取Surface ID
receiver.getReceivingSurfaceId((err, surfaceId) => {
// 获取Surface ID失败
if (err) {
console.info('getReceivingSurfaceId failed, err' + err);
return
}
console.info("receiver getReceivingSurfaceId success");
});
// 注册Surface的监听,在surface的buffer准备好后触发
receiver.on('imageArrival', () => {
// 去获取Surface中最新的buffer
receiver.readNextImage((err, img) => {
img.getComponent(4, (err, component) => {
// 消费component.byteBuffer,例如:将buffer内容保存成图片。
})
})
})
// 调用Camera方法将surfaceId传递给Camera。camera会通过surfaceId获取surface,并生产出surface buffer。
}
```
## 相关实例
针对图片开发,有以下相关实例可供参考:
- [`Image`:图片处理(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Media/Image)
- [`GamePuzzle`:拼图(ArkTS)(API9)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/Media/GamePuzzle)
\ No newline at end of file
# 本地媒体会话概述
## 交互过程
本地媒体会话的数据源均在设备本地,交互过程如图所示。
![Local AVSession Interaction Process](figures/local-avsession-interaction-process.png)
此过程中涉及两大角色,媒体会话提供方和媒体会话控制方。
本地媒体会话中,媒体会话提供方通过媒体会话管理器和媒体会话控制方进行信息交互:
1. 媒体会话提供方通过AVSessionManager创建AVSession对象。
2. 媒体会话提供方通过AVSession对象,设置会话元数据(媒体ID、标题、媒体时长等)、会话播放属性(播放状态、播放倍速、播放位置等)等。
3. 媒体会话控制方通过AVSessionManager创建AVSessionController对象。
4. 媒体会话控制方通过AVSessionController对象可以监听对应会话元数据变化、播放属性变化等。
5. 媒体会话控制方通过AVSessionController对象还可以向媒体会话发送控制命令。
6. 媒体会话提供方通过AVSession对象可以监听来自媒体会话控制方的控制命令,例如:“play”播放、“playNext”播放下一首、“fastForward”快进、 “setSpeed”设置播放倍数等。
## 媒体会话管理器
媒体会话管理器(AVSessionManager),提供了管理AVSession的能力,可以创建AVSession、创建AVSessionController、发送系统控制事件,也支持对AVSession的状态进行监听。
实际上,AVSessionManager与AVSession、AVSessionController对象不同,并不是一个具体的对象,它是媒体会话的根命名域。在实际编程过程中,可以通过如下方式引入:
```ts
import AVSessionManager from '@ohos.multimedia.avsession';
```
根命名域中的所有方法都可以作为AVSessionManager的方法。
例如,媒体会话提供方通过AVSessionManager创建媒体会话的示例如下所示:
```ts
// 创建session
async createSession() {
let session: AVSessionManager.AVSession = await AVSessionManager.createAVSession(this.context, 'SESSION_NAME', 'audio');
console.info(`session create done : sessionId : ${session.sessionId}`);
}
```
例如,媒体会话控制方通过AVSessionManager创建媒体会话控制器的示例如下所示:
```ts
// 创建controller
async createController() {
// 获取到所有存活session的描述符列表
let descriptorsArray: Array<Readonly<AVSessionManager.AVSessionDescriptor>> = await AVSessionManager.getAllSessionDescriptors();
if (descriptorsArray.length > 0) {
// 为了演示,我们简单取第一个描述符的sessionId用来创建对应的controller
let sessionId: string = descriptorsArray[0].sessionId;
let avSessionController: AVSessionManager.AVSessionController = await AVSessionManager.createController(sessionId);
console.info(`controller create done : sessionId : ${avSessionController.sessionId}`);
}
}
```
更多关于AVSessionManager的方法,可以参考[API文档](../reference/apis/js-apis-avsession.md)
# 媒体应用开发概述
## 媒体系统架构
媒体系统提供用户视觉、听觉信息的处理能力,如音视频信息的采集、压缩存储、解压播放等。 在操作系统实现中,通常基于不同的媒体信息处理内容,将媒体划分为音频子系统、视频子系统(也称播放录制子系统)、相机子系统、图片子系统或服务等。
如下图所示,媒体系统面向应用开发提供音视频应用、相机应用、图库应用的编程框架接口;面向设备开发提供对接不同硬件芯片适配加速功能;中间以服务形态提供媒体核心功能和管理机制。
**图1** 媒体系统整体框架  
![Media system framework](figures/media-system-framework.png)
- 音频子系统(audio):提供音量管理、音频路由管理、混音管理接口与服务。
- 视频子系统(media):提供音视频解压播放、压缩录制接口与服务。
- 相机子系统(camera):提供精确控制相机镜头,采集视觉信息的接口与服务。
- 图片子系统(image):提供图片编解码、图片处理接口与服务。
# 管理麦克风
因为在录制过程中需要使用麦克风录制相关音频数据,所以建议开发者在调用录制接口前查询麦克风状态,并在录制过程中监听麦克风的状态变化,避免影响录制效果。
在音频录制过程中,用户可以将麦克风静音,此时录音过程正常进行,录制生成的数据文件的大小随录制时长递增,但写入文件的数据均为0,即无声数据(空白数据)。
## 开发步骤及注意事项
在AudioVolumeGroupManager中提供了管理麦克风状态的方法,接口的详细说明请参考[API文档](../reference/apis/js-apis-audio.md#audiovolumegroupmanager9)
1. 创建audioVolumeGroupManager对象。
```ts
import audio from '@ohos.multimedia.audio';
let audioVolumeGroupManager;
async function loadVolumeGroupManager() { //创建audioVolumeGroupManager对象
const groupid = audio.DEFAULT_VOLUME_GROUP_ID;
audioVolumeGroupManager = await audio.getAudioManager().getVolumeManager().getVolumeGroupManager(groupid);
console.info('audioVolumeGroupManager create success.');
}
```
2. 调用on('micStateChange')监听麦克风状态变化,当麦克风静音状态发生变化时将通知应用。
目前此订阅接口在单进程多AudioManager实例的使用场景下,仅最后一个实例的订阅生效,其他实例的订阅会被覆盖(即使最后一个实例没有进行订阅),因此推荐使用单一AudioManager实例进行开发。
```ts
async function on() { //监听麦克风状态变化
audioVolumeGroupManager.on('micStateChange', (micStateChange) => {
console.info(`Current microphone status is: ${micStateChange.mute} `);
});
}
```
3. 调用isMicrophoneMute查询麦克风当前静音状态,返回true为静音,false为非静音。
```ts
async function isMicrophoneMute() { //查询麦克风是否静音
await audioVolumeGroupManager.isMicrophoneMute().then((value) => {
console.info(`isMicrophoneMute is: ${value}.`);
});
}
```
4. 根据查询结果的实际情况,调用setMicrophoneMute设置麦克风静音状态,入参输入true为静音,false为非静音。设置成功返回true,否则返回false。
```ts
async function setMicrophoneMuteTrue() { //设置麦克风静音,入参为true
await audioVolumeGroupManager.setMicrophoneMute(true).then(() => {
console.info('setMicrophoneMute to mute.');
});
}
async function setMicrophoneMuteFalse() { //取消麦克风静音,入参为false
await audioVolumeGroupManager.setMicrophoneMute(false).then(() => {
console.info('setMicrophoneMute to not mute.');
});
}
```
## 完整示例
参考以下示例,完成从设置麦克风静音到取消麦克风静音的过程。
```ts
import audio from '@ohos.multimedia.audio';
@Entry
@Component
struct AudioVolumeGroup {
private audioVolumeGroupManager: audio.AudioVolumeGroupManager;
async loadVolumeGroupManager() {
const groupid = audio.DEFAULT_VOLUME_GROUP_ID;
this.audioVolumeGroupManager = await audio.getAudioManager().getVolumeManager().getVolumeGroupManager(groupid);
console.info('audioVolumeGroupManager------create-------success.');
}
async on() { //监听麦克风状态变化
await this.loadVolumeGroupManager();
this.audioVolumeGroupManager.on('micStateChange', (micStateChange) => {
console.info(`Current microphone status is: ${micStateChange.mute} `);
});
}
async isMicrophoneMute() { //查询麦克风是否静音
await this.audioVolumeGroupManager.isMicrophoneMute().then((value) => {
console.info(`isMicrophoneMute is: ${value}.`);
});
}
async setMicrophoneMuteTrue() { //设置麦克风静音
await this.loadVolumeGroupManager();
await this.audioVolumeGroupManager.setMicrophoneMute(true).then(() => {
console.info('setMicrophoneMute to mute.');
});
}
async setMicrophoneMuteFalse() { //取消麦克风静音
await this.loadVolumeGroupManager();
await this.audioVolumeGroupManager.setMicrophoneMute(false).then(() => {
console.info('setMicrophoneMute to not mute.');
});
}
async test(){
await this.on();
await this.isMicrophoneMute();
await this.setMicrophoneMuteTrue();
await this.isMicrophoneMute();
await this.setMicrophoneMuteFalse();
await this.isMicrophoneMute();
await this.setMicrophoneMuteTrue();
await this.isMicrophoneMute();
}
}
```
# OpenSL ES音频录制开发指导
## 简介
开发者可以通过本文档了解在**OpenHarmony**中如何使用**OpenSL ES**进行录音相关操作;当前仅实现了部分[**OpenSL ES**接口](https://gitee.com/openharmony/third_party_opensles/blob/master/api/1.0.1/OpenSLES.h),因此调用未实现接口后会返回**SL_RESULT_FEATURE_UNSUPPORTED**
## 开发指导
以下步骤描述了在**OpenHarmony**如何使用 **OpenSL ES** 开发音频录音功能:
1. 添加头文件
```c++
#include <OpenSLES.h>
#include <OpenSLES_OpenHarmony.h>
#include <OpenSLES_Platform.h>
```
2. 使用 **slCreateEngine** 接口创建引擎对象和实例化引擎对象 **engine**
```c++
SLObjectItf engineObject = nullptr;
slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
```
3. 获取接口 **SL_IID_ENGINE** 的引擎接口 **engineEngine** 实例
```c++
SLEngineItf engineItf = nullptr;
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineItf);
```
4. 配置录音器信息(配置输入源audiosource、输出源audiosink),创建录音对象**pcmCapturerObject**
```c++
SLDataLocator_IODevice io_device = {
SL_DATALOCATOR_IODEVICE,
SL_IODEVICE_AUDIOINPUT,
SL_DEFAULTDEVICEID_AUDIOINPUT,
NULL
};
SLDataSource audioSource = {
&io_device,
NULL
};
SLDataLocator_BufferQueue buffer_queue = {
SL_DATALOCATOR_BUFFERQUEUE,
3
};
// 具体参数需要根据音频文件格式进行适配
SLDataFormat_PCM format_pcm = {
SL_DATAFORMAT_PCM, // 输入的音频格式
1, // 单声道
SL_SAMPLINGRATE_44_1, // 采样率,44100HZ
SL_PCMSAMPLEFORMAT_FIXED_16, // 音频采样格式,小尾数,带符号的16位整数
0,
0,
0
};
SLDataSink audioSink = {
&buffer_queue,
&format_pcm
};
SLObjectItf pcmCapturerObject = nullptr;
result = (*engineItf)->CreateAudioRecorder(engineItf, &pcmCapturerObject,
&audioSource, &audioSink, 0, nullptr, nullptr);
(*pcmCapturerObject)->Realize(pcmCapturerObject, SL_BOOLEAN_FALSE);
```
5. 获取录音接口**SL_IID_RECORD****recordItf** 接口实例
```c++
SLRecordItf recordItf;
(*pcmCapturerObject)->GetInterface(pcmCapturerObject, SL_IID_RECORD, &recordItf);
```
6. 获取接口 **SL_IID_OH_BUFFERQUEUE****bufferQueueItf** 实例
```c++
SLOHBufferQueueItf bufferQueueItf;
(*pcmCapturerObject)->GetInterface(pcmCapturerObject, SL_IID_OH_BUFFERQUEUE, &bufferQueueItf);
```
7. 注册 **BufferQueueCallback** 回调
```c++
static void BufferQueueCallback(SLOHBufferQueueItf bufferQueueItf, void *pContext, SLuint32 size)
{
AUDIO_INFO_LOG("BufferQueueCallback");
FILE *wavFile = (FILE *)pContext;
if (wavFile != nullptr) {
SLuint8 *buffer = nullptr;
SLuint32 pSize = 0;
(*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, pSize);
if (buffer != nullptr) {
fwrite(buffer, 1, pSize, wavFile);
(*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, size);
}
}
return;
}
// wavFile_ 需要设置为用户想要录音的文件描述符
(*bufferQueueItf)->RegisterCallback(bufferQueueItf, BufferQueueCallback, wavFile_);
```
8. 开始录音
```c++
static void CaptureStart(SLRecordItf recordItf, SLOHBufferQueueItf bufferQueueItf, FILE *wavFile)
{
AUDIO_INFO_LOG("CaptureStart");
(*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_RECORDING);
if (wavFile != nullptr) {
SLuint8* buffer = nullptr;
SLuint32 pSize = 0;
(*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, pSize);
if (buffer != nullptr) {
AUDIO_INFO_LOG("CaptureStart, enqueue buffer length: %{public}lu.", pSize);
fwrite(buffer, 1, pSize, wavFile);
(*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, pSize);
} else {
AUDIO_INFO_LOG("CaptureStart, buffer is null or pSize: %{public}lu.", pSize);
}
}
return;
}
```
9. 结束录音
```c++
static void CaptureStop(SLRecordItf recordItf)
{
AUDIO_INFO_LOG("Enter CaptureStop");
fflush(wavFile_);
(*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_STOPPED);
(*pcmCapturerObject)->Destroy(pcmCapturerObject);
fclose(wavFile_);
wavFile_ = nullptr;
return;
}
```
\ No newline at end of file
# OpenSL ES音频播放开发指导
## 简介
开发者可以通过本文档了解在**OpenHarmony**中如何使用**OpenSL ES**进行音频播放相关操作;当前仅实现了部分[**OpenSL ES**接口](https://gitee.com/openharmony/third_party_opensles/blob/master/api/1.0.1/OpenSLES.h),因此调用未实现接口后会返回**SL_RESULT_FEATURE_UNSUPPORTED**
## 开发指导
以下步骤描述了在**OpenHarmony**如何使用**OpenSL ES**开发音频播放功能:
1. 添加头文件
```c++
#include <OpenSLES.h>
#include <OpenSLES_OpenHarmony.h>
#include <OpenSLES_Platform.h>
```
2. 使用 **slCreateEngine** 接口和获取 **engine** 实例
```c++
SLObjectItf engineObject = nullptr;
slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
```
3. 获取接口 **SL_IID_ENGINE****engineEngine** 实例
```c++
SLEngineItf engineEngine = nullptr;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
```
4. 配置播放器信息,创建 **AudioPlayer**
```c++
SLDataLocator_BufferQueue slBufferQueue = {
SL_DATALOCATOR_BUFFERQUEUE,
0
};
// 具体参数需要根据音频文件格式进行适配
SLDataFormat_PCM pcmFormat = {
SL_DATAFORMAT_PCM,
2,
48000,
16,
0,
0,
0
};
SLDataSource slSource = {&slBufferQueue, &pcmFormat};
SLObjectItf pcmPlayerObject = nullptr;
(*engineEngine)->CreateAudioPlayer(engineEngine, &pcmPlayerObject, &slSource, null, 0, nullptr, nullptr);
(*pcmPlayerObject)->Realize(pcmPlayerObject, SL_BOOLEAN_FALSE);
```
5. 获取接口 **SL_IID_OH_BUFFERQUEUE****bufferQueueItf** 实例
```c++
SLOHBufferQueueItf bufferQueueItf;
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_OH_BUFFERQUEUE, &bufferQueueItf);
```
6. 打开音频文件,注册 **BufferQueueCallback** 回调
```c++
FILE *wavFile_ = nullptr;
static void BufferQueueCallback (SLOHBufferQueueItf bufferQueueItf, void *pContext, SLuint32 size)
{
FILE *wavFile = (FILE *)pContext;
if (!feof(wavFile)) {
SLuint8 *buffer = nullptr;
SLuint32 pSize = 0;
(*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, pSize);
//从文件读取数据
fread(buffer, 1, size, wavFile);
(*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, size);
}
return;
}
// wavFile_ 需要设置为用户想要播放的文件描述符
wavFile_ = fopen(path, "rb");
(*bufferQueueItf)->RegisterCallback(bufferQueueItf, BufferQueueCallback, wavFile_);
```
7. 获取接口 **SL_PLAYSTATE_PLAYING****playItf** 实例,开始播放
```c++
SLPlayItf playItf = nullptr;
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_PLAY, &playItf);
(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);
```
8. 结束音频播放
```c++
(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_STOPPED);
(*pcmPlayerObject)->Destroy(pcmPlayerObject);
(*engineObject)->Destroy(engineObject);
```
\ No newline at end of file
# 分布式相机开发指导
## 场景介绍
分布式相机模块支持相机相关基础功能介绍说明。
## 开发步骤
在计算器中连接分布式设备,在获取相机列表getSupportedCameras(),遍历返回的列表,判断对应Camera对象中的ConnectionType是否等于CAMERA_CONNECTION_REMOTE,若等于则使用此对象创建cameraInput,之后调用与本地相机使用一样。参考:[相机开发指导](./camera.md)
详细API含义请参考:[相机管理API文档](../reference/apis/js-apis-camera.md)
### 连接分布式相机
打开设置->WLAN,将需要连接分布式相机的两台设备连入同一局域网。
打开计算器,点击右上角小图标,出现新的窗口,按提示输入验证码,连接成功。
### 创建实例
```js
import camera from '@ohos.multimedia.camera'
import image from '@ohos.multimedia.image'
import media from '@ohos.multimedia.media'
import featureAbility from '@ohos.ability.featureAbility'
// 创建CameraManager对象
let cameraManager = camera.getCameraManager(globalThis.Context)
if (!cameraManager) {
console.error("camera.getCameraManager error")
return;
}
// 注册回调函数监听相机状态变化,获取状态变化的相机信息
cameraManager.on('cameraStatus', (cameraStatusInfo) => {
console.log('camera : ' + cameraStatusInfo.camera.cameraId);
console.log('status: ' + cameraStatusInfo.status);
})
// 获取相机列表
let remoteCamera
let cameraArray = cameraManager.getSupportedCameras();
if (cameraArray.length <= 0) {
console.error("cameraManager.getSupportedCameras error")
return;
}
for(let cameraIndex = 0; cameraIndex < cameraArray.length; cameraIndex++) {
console.log('cameraId : ' + cameraArray[cameraIndex].cameraId) // 获取相机ID
console.log('cameraPosition : ' + cameraArray[cameraIndex].cameraPosition) // 获取相机位置
console.log('cameraType : ' + cameraArray[cameraIndex].cameraType) // 获取相机类型
console.log('connectionType : ' + cameraArray[cameraIndex].connectionType) // 获取相机连接类型
if (cameraArray[cameraIndex].connectionType == CAMERA_CONNECTION_REMOTE) {
remoteCamera = cameraArray[cameraIndex]
}
}
// 创建相机输入流
let cameraInput
try {
cameraInput = cameraManager.createCameraInput(remoteCamera);
} catch () {
console.error('Failed to createCameraInput errorCode = ' + error.code);
}
```
剩余步骤参照[相机开发指导](./camera.md)
\ No newline at end of file
# 使用AudioCapturer开发音频录制功能
AudioCapturer是音频采集器,用于录制PCM(Pulse Code Modulation)音频数据,适合有音频开发经验的开发者实现更灵活的录制功能。
## 开发指导
使用AudioCapturer录制音频涉及到AudioCapturer实例的创建、音频采集参数的配置、采集的开始与停止、资源的释放等。本开发指导将以一次录制音频数据的过程为例,向开发者讲解如何使用AudioCapturer进行音频录制,建议搭配[AudioCapturer的API说明](../reference/apis/js-apis-audio.md#audiocapturer8)阅读。
下图展示了AudioCapturer的状态变化,在创建实例后,调用对应的方法可以进入指定的状态实现对应的行为。需要注意的是在确定的状态执行不合适的方法可能导致AudioCapturer发生错误,建议开发者在调用状态转换的方法前进行状态检查,避免程序运行产生预期以外的结果。
**图1** AudioCapturer状态变化示意图  
![AudioCapturer status change](figures/audiocapturer-status-change.png)
使用on('stateChange')方法可以监听AudioCapturer的状态变化,每个状态对应值与说明见[AudioState](../reference/apis/js-apis-audio.md#audiostate8)
### 开发步骤及注意事项
1. 配置音频采集参数并创建AudioCapturer实例,音频采集参数的详细信息可以查看[AudioCapturerOptions](../reference/apis/js-apis-audio.md#audiocaptureroptions8)
```ts
import audio from '@ohos.multimedia.audio';
let audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
channels: audio.AudioChannel.CHANNEL_2,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
let audioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC,
capturerFlags: 0
};
let audioCapturerOptions = {
streamInfo: audioStreamInfo,
capturerInfo: audioCapturerInfo
};
audio.createAudioCapturer(audioCapturerOptions, (err, data) => {
if (err) {
console.error(`Invoke createAudioCapturer failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info('Invoke createAudioCapturer succeeded.');
let audioCapturer = data;
}
});
```
2. 调用start()方法进入running状态,开始录制音频。
```ts
audioCapturer.start((err) => {
if (err) {
console.error(`Capturer start failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info('Capturer start success.');
}
});
```
3. 指定录制文件地址,调用read()方法读取缓冲区的数据。
```ts
let file = fs.openSync(path, 0o2 | 0o100);
let bufferSize = await audioCapturer.getBufferSize();
let buffer = await audioCapturer.read(bufferSize, true);
fs.writeSync(file.fd, buffer);
```
4. 调用stop()方法停止录制。
```ts
audioCapturer.stop((err) => {
if (err) {
console.error(`Capturer stop failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info('Capturer stopped.');
}
});
```
5. 调用release()方法销毁实例,释放资源。
```ts
audioCapturer.release((err) => {
if (err) {
console.error(`capturer release failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info('capturer released.');
}
});
```
### 完整示例
下面展示了使用AudioCapturer录制音频的完整示例代码。
```ts
import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs';
const TAG = 'AudioCapturerDemo';
export default class AudioCapturerDemo {
private audioCapturer = undefined;
private audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
}
private audioCapturerInfo = {
source: audio.SourceType.SOURCE_TYPE_MIC, // 音源类型
capturerFlags: 0 // 音频采集器标志
}
private audioCapturerOptions = {
streamInfo: this.audioStreamInfo,
capturerInfo: this.audioCapturerInfo
}
// 初始化,创建实例,设置监听事件
init() {
audio.createAudioCapturer(this.audioCapturerOptions, (err, capturer) => { // 创建AudioCapturer实例
if (err) {
console.error(`Invoke createAudioCapturer failed, code is ${err.code}, message is ${err.message}`);
return;
}
console.info(`${TAG}: create AudioCapturer success`);
this.audioCapturer = capturer;
this.audioCapturer.on('markReach', 1000, (position) => { // 订阅markReach事件,当采集的帧数达到1000时触发回调
if (position === 1000) {
console.info('ON Triggered successfully');
}
});
this.audioCapturer.on('periodReach', 2000, (position) => { // 订阅periodReach事件,当采集的帧数达到2000时触发回调
if (position === 2000) {
console.info('ON Triggered successfully');
}
});
});
}
// 开始一次音频采集
async start() {
let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
if (stateGroup.indexOf(this.audioCapturer.state) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动采集
console.error(`${TAG}: start failed`);
return;
}
await this.audioCapturer.start(); // 启动采集
let context = getContext(this);
const path = context.filesDir + '/test.wav'; // 采集到的音频文件存储路径
let file = fs.openSync(path, 0o2 | 0o100); // 如果文件不存在则创建文件
let fd = file.fd;
let numBuffersToCapture = 150; // 循环写入150次
let count = 0;
while (numBuffersToCapture) {
let bufferSize = await this.audioCapturer.getBufferSize();
let buffer = await this.audioCapturer.read(bufferSize, true);
let options = {
offset: count * bufferSize,
length: bufferSize
};
if (buffer === undefined) {
console.error(`${TAG}: read buffer failed`);
} else {
let number = fs.writeSync(fd, buffer, options);
console.info(`${TAG}: write date: ${number}`);
}
numBuffersToCapture--;
count++;
}
}
// 停止采集
async stop() {
// 只有采集器状态为STATE_RUNNING或STATE_PAUSED的时候才可以停止
if (this.audioCapturer.state !== audio.AudioState.STATE_RUNNING && this.audioCapturer.state !== audio.AudioState.STATE_PAUSED) {
console.info('Capturer is not running or paused');
return;
}
await this.audioCapturer.stop(); // 停止采集
if (this.audioCapturer.state === audio.AudioState.STATE_STOPPED) {
console.info('Capturer stopped');
} else {
console.error('Capturer stop failed');
}
}
// 销毁实例,释放资源
async release() {
// 采集器状态不是STATE_RELEASED或STATE_NEW状态,才能release
if (this.audioCapturer.state === audio.AudioState.STATE_RELEASED || this.audioCapturer.state === audio.AudioState.STATE_NEW) {
console.info('Capturer already released');
return;
}
await this.audioCapturer.release(); // 释放资源
if (this.audioCapturer.state == audio.AudioState.STATE_RELEASED) {
console.info('Capturer released');
} else {
console.error('Capturer release failed');
}
}
}
```
# 使用AudioRenderer开发音频播放功能
AudioRenderer是音频渲染器,用于播放PCM(Pulse Code Modulation)音频数据,相比AVPlayer而言,可以在输入前添加数据预处理,更适合有音频开发经验的开发者,以实现更灵活的播放功能。
## 开发指导
使用AudioRenderer播放音频涉及到AudioRenderer实例的创建、音频渲染参数的配置、渲染的开始与停止、资源的释放等。本开发指导将以一次渲染音频数据的过程为例,向开发者讲解如何使用AudioRenderer进行音频渲染,建议搭配[AudioRenderer的API说明](../reference/apis/js-apis-audio.md#audiorenderer8)阅读。
下图展示了AudioRenderer的状态变化,在创建实例后,调用对应的方法可以进入指定的状态实现对应的行为。需要注意的是在确定的状态执行不合适的方法可能导致AudioRenderer发生错误,建议开发者在调用状态转换的方法前进行状态检查,避免程序运行产生预期以外的结果。
为保证UI线程不被阻塞,大部分AudioRenderer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用callback函数。
**图1** AudioRenderer状态变化示意图  
![AudioRenderer status change](figures/audiorenderer-status-change.png)
在进行应用开发的过程中,建议开发者通过on('stateChange')方法订阅AudioRenderer的状态变更。因为针对AudioRenderer的某些操作,仅在音频播放器在固定状态时才能执行。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
- prepared状态: 通过调用createAudioRenderer()方法进入到该状态。
- running状态: 正在进行音频数据播放,可以在prepared状态通过调用start()方法进入此状态,也可以在pause状态和stopped状态通过调用start()方法进入此状态。
- paused状态: 在running状态可以通过调用pause()方法暂停音频数据的播放并进入paused状态,暂停播放之后可以通过调用start()方法继续音频数据播放。
- stopped状态: 在paused/running状态可以通过stop()方法停止音频数据的播放。
- released状态: 在prepared、paused、stopped等状态,用户均可通过release()方法释放掉所有占用的硬件和软件资源,并且不会再进入到其他的任何一种状态了。
### 开发步骤及注意事项
1. 配置音频渲染参数并创建AudioRenderer实例,音频渲染参数的详细信息可以查看[AudioRendererOptions](../reference/apis/js-apis-audio.md#audiorendereroptions8)
```ts
import audio from '@ohos.multimedia.audio';
let audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_44100,
channels: audio.AudioChannel.CHANNEL_1,
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE,
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW
};
let audioRendererInfo = {
content: audio.ContentType.CONTENT_TYPE_SPEECH,
usage: audio.StreamUsage.STREAM_USAGE_VOICE_COMMUNICATION,
rendererFlags: 0
};
let audioRendererOptions = {
streamInfo: audioStreamInfo,
rendererInfo: audioRendererInfo
};
audio.createAudioRenderer(audioRendererOptions, (err, data) => {
if (err) {
console.error(`Invoke createAudioRenderer failed, code is ${err.code}, message is ${err.message}`);
return;
} else {
console.info('Invoke createAudioRenderer succeeded.');
let audioRenderer = data;
}
});
```
2. 调用start()方法进入running状态,开始渲染音频。
```ts
audioRenderer.start((err) => {
if (err) {
console.error(`Renderer start failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info('Renderer start success.');
}
});
```
3. 指定待渲染文件地址,打开文件调用write()方法向缓冲区持续写入音频数据进行渲染播放。如果需要对音频数据进行处理以实现个性化的播放,在写入之前操作即可。
```ts
const bufferSize = await audioRenderer.getBufferSize();
let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
let buf = new ArrayBuffer(bufferSize);
let readsize = await fs.read(file.fd, buf);
let writeSize = await new Promise((resolve, reject) => {
audioRenderer.write(buf, (err, writeSize) => {
if (err) {
reject(err);
} else {
resolve(writeSize);
}
});
});
```
4. 调用stop()方法停止渲染。
```ts
audioRenderer.stop((err) => {
if (err) {
console.error(`Renderer stop failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info('Renderer stopped.');
}
});
```
5. 调用release()方法销毁实例,释放资源。
```ts
audioRenderer.release((err) => {
if (err) {
console.error(`Renderer release failed, code is ${err.code}, message is ${err.message}`);
} else {
console.info('Renderer released.');
}
});
```
### 完整示例
下面展示了使用AudioRenderer渲染音频文件的示例代码。
```ts
import audio from '@ohos.multimedia.audio';
import fs from '@ohos.file.fs';
const TAG = 'AudioRendererDemo';
export default class AudioRendererDemo {
private renderModel = undefined;
private audioStreamInfo = {
samplingRate: audio.AudioSamplingRate.SAMPLE_RATE_48000, // 采样率
channels: audio.AudioChannel.CHANNEL_2, // 通道数
sampleFormat: audio.AudioSampleFormat.SAMPLE_FORMAT_S16LE, // 采样格式
encodingType: audio.AudioEncodingType.ENCODING_TYPE_RAW // 编码格式
}
private audioRendererInfo = {
content: audio.ContentType.CONTENT_TYPE_MUSIC, // 媒体类型
usage: audio.StreamUsage.STREAM_USAGE_MEDIA, // 音频流使用类型
rendererFlags: 0 // 音频渲染器标志
}
private audioRendererOptions = {
streamInfo: this.audioStreamInfo,
rendererInfo: this.audioRendererInfo
}
// 初始化,创建实例,设置监听事件
init() {
audio.createAudioRenderer(this.audioRendererOptions, (err, renderer) => { // 创建AudioRenderer实例
if (!err) {
console.info(`${TAG}: creating AudioRenderer success`);
this.renderModel = renderer;
this.renderModel.on('stateChange', (state) => { // 设置监听事件,当转换到指定的状态时触发回调
if (state == 1) {
console.info('audio renderer state is: STATE_PREPARED');
}
if (state == 2) {
console.info('audio renderer state is: STATE_RUNNING');
}
});
this.renderModel.on('markReach', 1000, (position) => { // 订阅markReach事件,当渲染的帧数达到1000帧时触发回调
if (position == 1000) {
console.info('ON Triggered successfully');
}
});
} else {
console.info(`${TAG}: creating AudioRenderer failed, error: ${err.message}`);
}
});
}
// 开始一次音频渲染
async start() {
let stateGroup = [audio.AudioState.STATE_PREPARED, audio.AudioState.STATE_PAUSED, audio.AudioState.STATE_STOPPED];
if (stateGroup.indexOf(this.renderModel.state) === -1) { // 当且仅当状态为prepared、paused和stopped之一时才能启动渲染
console.error(TAG + 'start failed');
return;
}
await this.renderModel.start(); // 启动渲染
const bufferSize = await this.renderModel.getBufferSize();
let context = getContext(this);
let path = context.filesDir;
const filePath = path + '/test.wav'; // 使用沙箱路径获取文件,实际路径为/data/storage/el2/base/haps/entry/files/test.wav
let file = fs.openSync(filePath, fs.OpenMode.READ_ONLY);
let stat = await fs.stat(filePath);
let buf = new ArrayBuffer(bufferSize);
let len = stat.size % bufferSize === 0 ? Math.floor(stat.size / bufferSize) : Math.floor(stat.size / bufferSize + 1);
for (let i = 0; i < len; i++) {
let options = {
offset: i * bufferSize,
length: bufferSize
};
let readsize = await fs.read(file.fd, buf, options);
// buf是要写入缓冲区的音频数据,在调用AudioRenderer.write()方法前可以进行音频数据的预处理,实现个性化的音频播放功能,AudioRenderer会读出写入缓冲区的音频数据进行渲染
let writeSize = await new Promise((resolve, reject) => {
this.renderModel.write(buf, (err, writeSize) => {
if (err) {
reject(err);
} else {
resolve(writeSize);
}
});
});
if (this.renderModel.state === audio.AudioState.STATE_RELEASED) { // 如果渲染器状态为released,停止渲染
fs.close(file);
await this.renderModel.stop();
}
if (this.renderModel.state === audio.AudioState.STATE_RUNNING) {
if (i === len - 1) { // 如果音频文件已经被读取完,停止渲染
fs.close(file);
await this.renderModel.stop();
}
}
}
}
// 暂停渲染
async pause() {
// 只有渲染器状态为running的时候才能暂停
if (this.renderModel.state !== audio.AudioState.STATE_RUNNING) {
console.info('Renderer is not running');
return;
}
await this.renderModel.pause(); // 暂停渲染
if (this.renderModel.state === audio.AudioState.STATE_PAUSED) {
console.info('Renderer is paused.');
} else {
console.error('Pausing renderer failed.');
}
}
// 停止渲染
async stop() {
// 只有渲染器状态为running或paused的时候才可以停止
if (this.renderModel.state !== audio.AudioState.STATE_RUNNING && this.renderModel.state !== audio.AudioState.STATE_PAUSED) {
console.info('Renderer is not running or paused.');
return;
}
await this.renderModel.stop(); // 停止渲染
if (this.renderModel.state === audio.AudioState.STATE_STOPPED) {
console.info('Renderer stopped.');
} else {
console.error('Stopping renderer failed.');
}
}
// 销毁实例,释放资源
async release() {
// 渲染器状态不是released状态,才能release
if (this.renderModel.state === audio.AudioState.STATE_RELEASED) {
console.info('Renderer already released');
return;
}
await this.renderModel.release(); // 释放资源
if (this.renderModel.state === audio.AudioState.STATE_RELEASED) {
console.info('Renderer released');
} else {
console.error('Renderer release failed.');
}
}
}
```
当同优先级或高优先级音频流要使用输出设备时,当前音频流会被中断,应用可以自行响应中断事件并做出处理。具体的音频并发处理方式可参考[多音频播放的并发策略](audio-playback-concurrency.md)
# 使用AVPlayer开发音频播放功能
使用AVPlayer可以实现端到端播放原始媒体资源,本开发指导将以完整地播放一首音乐作为示例,向开发者讲解AVPlayer音频播放相关功能。
以下指导仅介绍如何实现媒体资源播放,如果要实现后台播放或熄屏播放,需要使用[AVSession(媒体会话)](avsession-overview.md)[申请长时任务](../task-management/continuous-task-dev-guide.md),避免播放被系统强制中断。
播放的全流程包含:创建AVPlayer,设置播放资源,设置播放参数(音量/倍速/焦点模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。
在进行应用开发的过程中,开发者可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')方法监听状态变化。如果应用在音频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
**图1** 播放状态变化示意图  
![Playback status change](figures/playback-status-change.png)
状态的详细说明请参考[AVPlayerState](../reference/apis/js-apis-media.md#avplayerstate9)。当播放处于prepared / playing / paused / completed状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存。当客户端暂时不使用播放器时,调用reset()或release()回收内存资源,做好资源利用。
## 开发步骤及注意事项
详细的API说明请参考[AVPlayer API参考](../reference/apis/js-apis-media.md#avplayer9)
1. 创建实例createAVPlayer(),AVPlayer初始化idle状态。
2. 设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括:
| 事件类型 | 说明 |
| -------- | -------- |
| stateChange | 必要事件,监听播放器的state属性改变。 |
| error | 必要事件,监听播放器的错误信息。 |
| durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 |
| timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 |
| seekDone | 响应API调用,监听seek()请求完成情况。<br/>当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。 |
| speedDone | 响应API调用,监听setSpeed()请求完成情况。<br/>当使用setSpeed()设置播放倍速后,如果setSpeed操作成功,将上报该事件。 |
| volumeChange | 响应API调用,监听setVolume()请求完成情况。<br/>当使用setVolume()调节播放音量后,如果setVolume操作成功,将上报该事件。 |
| bufferingUpdate | 用于网络播放,监听网络播放缓冲信息,用于上报缓冲百分比以及缓存播放进度。 |
| audioInterrupt | 监听音频焦点切换信息,搭配属性audioInterruptMode使用。<br/>如果当前设备存在多个音频正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 |
3. 设置资源:设置属性url,AVPlayer进入initialized状态。
> **说明:**
>
> 下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置:
>
> - 如果使用本地资源播放,必须确认资源文件可用,并使用应用沙箱路径访问对应资源,参考[获取应用文件路径](../application-models/application-context-stage.md#获取应用开发路径)。应用沙箱的介绍及如何向应用沙箱推送文件,请参考[文件管理](../file-management/app-sandbox-directory.md)。
>
> - 如果使用网络播放路径,需[申请相关权限](../security/accesstoken-guidelines.md):ohos.permission.INTERNET。
>
> - 如果使用ResourceManager.getRawFd打开HAP资源文件描述符,使用方法可参考[ResourceManager API参考](../reference/apis/js-apis-resource-manager.md#getrawfd9)。
>
> - 需要使用[支持的播放格式与协议](avplayer-avrecorder-overview.md#支持的格式与协议)。
4. 准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置音量。
5. 音频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。
6. (可选)更换资源:调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。
7. 退出播放:调用release()销毁实例,AVPlayer进入released状态,退出播放。
## 完整示例
参考以下示例,完整地播放一首音乐。
```ts
import media from '@ohos.multimedia.media';
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
export class AVPlayerDemo {
private avPlayer;
private count: number = 0;
// 注册avplayer回调函数
setAVPlayerCallback() {
// seek操作结果回调函数
this.avPlayer.on('seekDone', (seekDoneTime) => {
console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
})
// error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
this.avPlayer.on('error', (err) => {
console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
this.avPlayer.reset(); // 调用reset重置资源,触发idle状态
})
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state, reason) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info('AVPlayer state idle called.');
this.avPlayer.release(); // 调用release接口销毁实例对象
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.info('AVPlayerstate initialized called.');
this.avPlayer.prepare().then(() => {
console.info('AVPlayer prepare succeeded.');
}, (err) => {
console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`);
});
break;
case 'prepared': // prepare调用成功后上报该状态机
console.info('AVPlayer state prepared called.');
this.avPlayer.play(); // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
console.info('AVPlayer state playing called.');
if (this.count !== 0) {
console.info('AVPlayer start to seek.');
this.avPlayer.seek(this.avPlayer.duration); //seek到音频末尾
} else {
this.avPlayer.pause(); // 调用暂停接口暂停播放
}
this.count++;
break;
case 'paused': // pause成功调用后触发该状态机上报
console.info('AVPlayer state paused called.');
this.avPlayer.play(); // 再次播放接口开始播放
break;
case 'completed': // 播放结束后触发该状态机上报
console.info('AVPlayer state completed called.');
this.avPlayer.stop(); //调用播放结束接口
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
console.info('AVPlayer state stopped called.');
this.avPlayer.reset(); // 调用reset接口初始化avplayer状态
break;
case 'released':
console.info('AVPlayer state released called.');
break;
default:
console.info('AVPlayer state unknown called.');
break;
}
})
}
// 以下demo为使用fs文件系统打开沙箱地址获取媒体文件地址并通过url属性进行播放示例
async avPlayerUrlDemo() {
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数
this.setAVPlayerCallback();
let fdPath = 'fd://';
// 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例
let context = getContext(this) as common.UIAbilityContext;
let pathDir = context.filesDir;
let path = pathDir + '/01.mp3';
// 打开相应的资源文件地址获取fd,并为url赋值触发initialized状态机上报
let file = await fs.open(path);
fdPath = fdPath + '' + file.fd;
this.avPlayer.url = fdPath;
}
// 以下demo为使用资源管理接口获取打包在HAP内的媒体资源文件并通过fdSrc属性进行播放示例
async avPlayerFdSrcDemo() {
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数
this.setAVPlayerCallback();
// 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址
// 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度
let context = getContext(this) as common.UIAbilityContext;
let fileDescriptor = await context.resourceManager.getRawFd('01.mp3');
// 为fdSrc赋值触发initialized状态机上报
this.avPlayer.fdSrc = fileDescriptor;
}
}
```
# 使用AVRecorder开发音频录制功能
使用AVRecorder可以实现音频录制功能,本开发指导将以“开始录制-暂停录制-恢复录制-停止录制”的一次流程为示例,向开发者讲解AVRecorder音频录制相关功能。
在进行应用开发的过程中,开发者可以通过AVRecorder的state属性,主动获取当前状态或使用on('stateChange')方法监听状态变化。开发过程中应该严格遵循状态机要求,例如只能在started状态下调用pause()接口,只能在paused状态下调用resume()接口。
**图1** 录制状态变化示意图  
![Recording status change](figures/recording-status-change.png)
状态的详细说明请参考[AVRecorderState](../reference/apis/js-apis-media.md#avrecorderstate9)
## 开发步骤及注意事项
详细的API说明请参考[AVRecorder API参考](../reference/apis/js-apis-media.md#avrecorder9)
1. 创建AVRecorder实例,实例创建完成进入idle状态。
```ts
import media from '@ohos.multimedia.media';
let avRecorder = undefined;
media.createAVRecorder().then((recorder) => {
avRecorder = recorder;
}, (err) => {
console.error(`Invoke createAVRecorder failed, code is ${err.code}, message is ${err.message}`);
})
```
2. 设置业务需要的监听事件,监听状态变化及错误上报。
| 事件类型 | 说明 |
| -------- | -------- |
| stateChange | 必要事件,监听播放器的state属性改变 |
| error | 必要事件,监听播放器的错误信息 |
```ts
// 状态上报回调函数
avRecorder.on('stateChange', (state, reason) => {
console.log(`current state is ${state}`);
// 用户可以在此补充状态发生切换后想要进行的动作
})
// 错误上报回调函数
avRecorder.on('error', (err) => {
console.error(`avRecorder failed, code is ${err.code}, message is ${err.message}`);
})
```
3. 配置音频录制参数,调用prepare()接口,此时进入prepared状态。
> **说明:**
> 配置参数需要注意:
>
> - prepare接口的入参avConfig中仅设置音频相关的配置参数,如示例代码所示。
> 如果只需要录制音频,请不要设置视频相关配置参数;如果需要录制视频,可以参考[视频录制开发指导](video-recording.md)进行开发。直接设置视频相关参数会导致后续步骤报错。
>
> - 需要使用支持的[录制规格](avplayer-avrecorder-overview.md#支持的格式)。
>
> - 录制输出的url地址(即示例里avConfig中的url),形式为fd://xx (fd number)。需要基础文件操作接口([ohos.file.fs](../reference/apis/js-apis-file-fs.md))实现应用文件访问能力,获取方式参考[应用文件访问与管理](../file-management/app-file-access.md)。
```ts
let avProfile = {
audioBitrate: 100000, // 音频比特率
audioChannels: 2, // 音频声道数
audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
audioSampleRate: 48000, // 音频采样率
fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
}
let avConfig = {
audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风
profile: avProfile,
url: 'fd://35', // 参考应用文件访问与管理中的开发示例获取创建的音频文件fd填入此处
}
avRecorder.prepare(avConfig).then(() => {
console.log('Invoke prepare succeeded.');
}, (err) => {
console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`);
})
```
4. 开始录制,调用start()接口,此时进入started状态。
5. 暂停录制,调用pause()接口,此时进入paused状态。
6. 恢复录制,调用resume()接口,此时再次进入started状态。
7. 停止录制,调用stop()接口,此时进入stopped状态。
8. 重置资源,调用reset()重新进入idle状态,允许重新配置录制参数。
9. 销毁实例,调用release()进入released状态,退出录制。
## 完整示例
参考以下示例,完成“开始录制-暂停录制-恢复录制-停止录制”的完整流程。
```ts
import media from '@ohos.multimedia.media';
export class AudioRecorderDemo {
private avRecorder;
private avProfile = {
audioBitrate: 100000, // 音频比特率
audioChannels: 2, // 音频声道数
audioCodec: media.CodecMimeType.AUDIO_AAC, // 音频编码格式,当前只支持aac
audioSampleRate: 48000, // 音频采样率
fileFormat: media.ContainerFormatType.CFT_MPEG_4A, // 封装格式,当前只支持m4a
};
private avConfig = {
audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC, // 音频输入源,这里设置为麦克风
profile: this.avProfile,
url: 'fd://35', // 参考应用文件访问与管理开发示例新建并读写一个文件
};
// 注册audioRecorder回调函数
setAudioRecorderCallback() {
// 状态机变化回调函数
this.avRecorder.on('stateChange', (state, reason) => {
console.log(`AudioRecorder current state is ${state}`);
})
// 错误上报回调函数
this.avRecorder.on('error', (err) => {
console.error(`AudioRecorder failed, code is ${err.code}, message is ${err.message}`);
})
}
// 开始录制对应的流程
async startRecordingProcess() {
// 1.创建录制实例
this.avRecorder = await media.createAVRecorder();
this.setAudioRecorderCallback();
// 2.获取录制文件fd赋予avConfig里的url;参考FilePicker文档
// 3.配置录制参数完成准备工作
await this.avRecorder.prepare(this.avConfig);
// 4.开始录制
await this.avRecorder.start();
}
// 暂停录制对应的流程
async pauseRecordingProcess() {
if (this.avRecorder.state === 'started') { // 仅在started状态下调用pause为合理状态切换
await this.avRecorder.pause();
}
}
// 恢复录制对应的流程
async resumeRecordingProcess() {
if (this.avRecorder.state === 'paused') { // 仅在paused状态下调用resume为合理状态切换
await this.avRecorder.resume();
}
}
// 停止录制对应的流程
async stopRecordingProcess() {
// 1. 停止录制
if (this.avRecorder.state === 'started'
|| this.avRecorder.state === 'paused') { // 仅在started或者paused状态下调用stop为合理状态切换
await this.avRecorder.stop();
}
// 2.重置
await this.avRecorder.reset();
// 3.释放录制实例
await this.avRecorder.release();
// 4.关闭录制文件fd
}
// 一个完整的【开始录制-暂停录制-恢复录制-停止录制】示例
async audioRecorderDemo() {
await this.startRecordingProcess(); // 开始录制
// 用户此处可以自行设置录制时长,例如通过设置休眠阻止代码执行
await this.pauseRecordingProcess(); //暂停录制
await this.resumeRecordingProcess(); // 恢复录制
await this.stopRecordingProcess(); // 停止录制
}
}
```
# 媒体会话控制方
OpenHarmony系统预置的播控中心,作为媒体会话控制方与音视频应用进行交互,包括获取媒体信息进行展示以及下发播控命令等。
系统应用开发者也可以根据需要,按照本章节的内容自行开发一款新的系统应用(例如新开发一款播控中心或语音助手),作为媒体会话控制方的角色,与系统中的音视频应用进行交互。
## 基本概念
- 媒体会话描述符(AVSessionDescriptor):描述媒体会话的相关信息,包含标识媒体会话的ID(sessionId),媒体会话的类型type(音频Audio/视频Video),媒体会话自定义名称(sessionTag),媒体会话所属应用的信息(elementName)、是否为置顶会话(isTopSession)等。
- 置顶会话(TopSession):系统中优先级最高的媒体会话,例如当前处于正在播放状态的会话。一般来说,如果想与媒体会话通信,需要获取会话对应的控制器,而媒体会话控制方可以在不用获取对应控制器的情况下,直接与置顶会话通信,例如直接向置顶会话发送播控命令和按键事件。
## 接口说明
媒体会话控制方使用的关键接口如下表所示。接口返回值有两种返回形式:callback和promise,下表中为callback形式接口,promise和callback只是返回值方式不一样,功能相同。
更多API说明请参见[API文档](../reference/apis/js-apis-avsession.md)
| 接口名 | 说明 |
| -------- | -------- |
| getAllSessionDescriptors(callback: AsyncCallback&lt;Array&lt;Readonly&lt;AVSessionDescriptor&gt;&gt;&gt;): void | 获取系统中所有媒体会话的描述符。 |
| createController(sessionId: string, callback: AsyncCallback&lt;AVSessionController&gt;): void | 创建媒体会话控制器。 |
| getValidCommands(callback: AsyncCallback&lt;Array&lt;AVControlCommandType&gt;&gt;): void | 获取媒体会话支持的有效命令。<br/>音视频应用在接入媒体会话时监听的播控命令,即为媒体会话支持的有效命令,相关信息请参见[媒体会话提供方监听播控命令事件](using-avsession-developer.md)。 |
| getLaunchAbility(callback: AsyncCallback&lt;WantAgent&gt;): void | 获取媒体会话中配置的可被拉起的UIAbility。<br/>当用户在媒体会话控制方应用进行界面操作,例如点击了播控中心卡片后,可以拉起对应的应用。 |
| sendAVKeyEvent(event: KeyEvent, callback: AsyncCallback&lt;void&gt;): void | 通过会话对应的AVSessionController向会话发送按键命令。 |
| sendSystemAVKeyEvent(event: KeyEvent, callback: AsyncCallback&lt;void&gt;): void | 向置顶会话发送按键命令。 |
| sendControlCommand(command: AVControlCommand, callback: AsyncCallback&lt;void&gt;): void | 通过会话对应的AVSessionController向会话发送播控命令。 |
| sendSystemControlCommand(command: AVControlCommand, callback: AsyncCallback&lt;void&gt;): void | 向置顶会话发送播控命令。 |
## 开发步骤
系统应用作为媒体会话控制方接入媒体会话的基本步骤如下所示:
1. 通过AVSessionManager获取媒体会话描述符AVSessionDescriptor,创建媒体会话控制器AVSessionController。
媒体会话控制方可以获取当前系统中所有的AVSessionDescriptor,并创建每个会话对应的AVSessionController,从而对系统中的音视频应用进行统一的播放控制。
```ts
//导入AVSession模块
import AVSessionManager from '@ohos.multimedia.avsession';
// 全局变量定义
let g_controller = new Array<AVSessionManager.AVSessionController>();
let g_centerSupportCmd:Set<AVSessionManager.AVControlCommandType> = new Set(['play', 'pause', 'playNext', 'playPrevious', 'fastForward', 'rewind', 'seek','setSpeed', 'setLoopMode', 'toggleFavorite']);
let g_validCmd:Set<AVSessionManager.AVControlCommandType>;
// 获取会话描述符,创建控制器
AVSessionManager.getAllSessionDescriptors().then((descriptors) => {
descriptors.forEach((descriptor) => {
AVSessionManager.createController(descriptor.sessionId).then((controller) => {
g_controller.push(controller);
}).catch((err) => {
console.error(`createController : ERROR : ${err.message}`);
});
});
}).catch((err) => {
console.error(`getAllSessionDescriptors : ERROR : ${err.message}`);
});
```
2. 监听AVSession会话状态及AVSession服务状态事件。
AVSession会话状态事件包括:
- sessionCreate:媒体会话创建事件。
- sessionDestroy:媒体会话销毁事件。
- topSessionChange:置顶会话发生变化事件。
AVSession服务状态事件指sessionServiceDie,在AVSession服务异常时产生该事件。
```ts
// 注册会话创建监听,创建控制器
AVSessionManager.on('sessionCreate', (session) => {
// 新增会话,需要创建控制器
AVSessionManager.createController(session.sessionId).then((controller) => {
g_controller.push(controller);
}).catch((err) => {
console.info(`createController : ERROR : ${err.message}`);
});
});
// 注册系统会话销毁监听
AVSessionManager.on('sessionDestroy', (session) => {
let index = g_controller.findIndex((controller) => {
return controller.sessionId === session.sessionId;
});
if (index !== 0) {
g_controller[index].destroy();
g_controller.splice(index, 1);
}
});
// 注册系统最高优先级会话变更监听
AVSessionManager.on('topSessionChange', (session) => {
let index = g_controller.findIndex((controller) => {
return controller.sessionId === session.sessionId;
});
// 将该会话显示排到第一个
if (index !== 0) {
g_controller.sort((a, b) => {
return a.sessionId === session.sessionId ? -1 : 0;
});
}
});
// 注册服务异常监听
AVSessionManager.on('sessionServiceDie', () => {
// 服务端异常,应用清理资源
console.info("服务端异常");
})
```
3. 监听媒体信息变化及会话其他事件。
AVSession媒体信息变化事件主要包括:
- metadataChange:媒体会话元数据变化事件。
- playbackStateChange:媒体播放状态变化事件。
- activeStateChange:媒体会话激活状态变化事件。
- validCommandChange:媒体会话支持的有效命令变化事件。
- outputDeviceChange:播放设备变化事件。
- sessionDestroy:媒体会话销毁事件。
媒体会话控制方可以根据实际需要监听对应的事件。
```ts
// 注册会话激活状态变更监听
controller.on('activeStateChange', (isActive) => {
if (isActive) {
console.info("控制器卡片按键高亮");
} else {
console.info("控制器卡片按键变更为无效");
}
});
// 注册会话销毁监听
controller.on('sessionDestroy', () => {
console.info('on sessionDestroy : SUCCESS ');
controller.destroy().then(() => {
console.info('destroy : SUCCESS ');
}).catch((err) => {
console.info(`destroy : ERROR :${err.message}`);
});
});
// 注册元数据更新监听
let metaFilter = ['assetId', 'title', 'description'];
controller.on('metadataChange', metaFilter, (metadata) => {
console.info(`on metadataChange assetId : ${metadata.assetId}`);
});
// 注册播放状态更新监听
let playbackFilter = ['state', 'speed', 'loopMode'];
controller.on('playbackStateChange', playbackFilter, (playbackState) => {
console.info(`on playbackStateChange state : ${playbackState.state}`);
});
// 注册会话支持的命令变更监听
controller.on('validCommandChange', (cmds) => {
console.info(`validCommandChange : SUCCESS : size : ${cmds.size}`);
console.info(`validCommandChange : SUCCESS : cmds : ${cmds.values()}`);
g_validCmd.clear();
for (let c of g_centerSupportCmd) {
if (cmds.has(c)) {
g_validCmd.add(c);
}
}
});
// 注册输出设备变更监听
controller.on('outputDeviceChange', (device) => {
console.info(`on outputDeviceChange device isRemote : ${device.isRemote}`);
});
```
4. 获取媒体会话提供方传递的媒体信息,可以用于界面展示,例如在播控中心展示当前播放的曲目及对应的播放状态。
```ts
async getInfoFromSessionByController() {
// 假设我们已经有了一个对应session的controller,如何创建controller可以参考之前的案例
let controller: AVSessionManager.AVSessionController = ALLREADY_HAVE_A_CONTROLLER;
// 获取sessionId
let sessionId: string = controller.sessionId;
console.info(`get sessionId by controller : isActive : ${sessionId}`);
// 获取session激活状态
let isActive: boolean = await controller.isActive();
console.info(`get activeState by controller : ${isActive}`);
// 获取session的媒体信息
let metadata: AVSessionManager.AVMetadata = await controller.getAVMetadata();
console.info(`get media title by controller : ${metadata.title}`);
console.info(`get media artist by controller : ${metadata.artist}`);
// 获取session的播放信息
let avPlaybackState: AVSessionManager.AVPlaybackState = await controller.getAVPlaybackState();
console.info(`get playbackState by controller : ${avPlaybackState.state}`);
console.info(`get favoriteState by controller : ${avPlaybackState.isFavorite}`);
}
```
5. 控制媒体会话行为,例如发送用户在播控中心对当前曲目的操作(播放/暂停/上一首/下一首等)命令。
作为媒体会话提供方的音视频应用在监听到相关的播控命令事件后,在相应的逻辑中可以完成对应的操作动作。
```ts
async sendCommandToSessionByController() {
// 假设我们已经有了一个对应session的controller,如何创建controller可以参考之前的案例
let controller: AVSessionManager.AVSessionController = ALLREADY_HAVE_A_CONTROLLER;
// 获取这个session支持的命令种类
let validCommandTypeArray: Array<AVSessionManager.AVControlCommandType> = await controller.getValidCommands();
console.info(`get validCommandArray by controller : length : ${validCommandTypeArray.length}`);
// 下发播放命令
// 如果可用命令包含播放,则下发播放命令,当然正常session都应该提供并实现播放功能吧
if (validCommandTypeArray.indexOf('play') >= 0) {
let avCommand: AVSessionManager.AVControlCommand = {command:'play'};
controller.sendControlCommand(avCommand);
}
// 下发暂停命令
if (validCommandTypeArray.indexOf('pause') >= 0) {
let avCommand: AVSessionManager.AVControlCommand = {command:'pause'};
controller.sendControlCommand(avCommand);
}
// 下发上一首命令
if (validCommandTypeArray.indexOf('playPrevious') >= 0) {
let avCommand: AVSessionManager.AVControlCommand = {command:'playPrevious'};
controller.sendControlCommand(avCommand);
}
// 下发下一首命令
if (validCommandTypeArray.indexOf('playNext') >= 0) {
let avCommand: AVSessionManager.AVControlCommand = {command:'playNext'};
controller.sendControlCommand(avCommand);
}
}
```
6. 在媒体会话控制方应用退出时及时取消事件监听,并释放资源。
```ts
async destroyController() {
// 假设我们已经有了一个对应session的controller,如何创建controller可以参考之前的案例
let controller: AVSessionManager.AVSessionController = ALLREADY_HAVE_A_CONTROLLER;
// 销毁当前的controller,销毁后这个controller将不在可用
controller.destroy(function (err) {
if (err) {
console.info(`Destroy controller ERROR : code: ${err.code}, message: ${err.message}`);
} else {
console.info('Destroy controller SUCCESS');
}
});
}
```
# 媒体会话提供方
音视频应用在实现音视频功能的同时,需要作为媒体会话提供方接入媒体会话,在媒体会话控制方(例如播控中心)中展示媒体相关信息,及响应媒体会话控制方下发的播控命令。
## 基本概念
- 媒体会话元数据(AVMetadata): 用于描述媒体数据相关属性,包含标识当前媒体的ID(assetId),上一首媒体的ID(previousAssetId),下一首媒体的ID(nextAssetId),标题(title),专辑作者(author),专辑名称(album),词作者(writer),媒体时长(duration)等属性。
- 媒体播放状态(AVPlaybackState):用于描述媒体播放状态的相关属性,包含当前媒体的播放状态(state)、播放位置(position)、播放倍速(speed)、缓冲时间(bufferedTime)、循环模式(loopMode)、是否收藏(isFavorite)等属性。
## 接口说明
媒体会话提供方使用的关键接口如下表所示。接口返回值有两种返回形式:callback和promise,下表中为callback形式接口,promise和callback只是返回值方式不一样,功能相同。
更多API说明请参见[API文档](../reference/apis/js-apis-avsession.md)
| 接口名 | 说明 |
| -------- | -------- |
| createAVSession(context: Context, tag: string, type: AVSessionType, callback: AsyncCallback&lt;AVSession&gt;): void | 创建媒体会话。<br/>一个UIAbility只能存在一个媒体会话,重复创建会失败。 |
| setAVMetadata(data: AVMetadata, callback: AsyncCallback&lt;void&gt;): void | 设置媒体会话元数据。 |
| setAVPlaybackState(state: AVPlaybackState, callback: AsyncCallback&lt;void&gt;): void | 设置媒体会话播放状态。 |
| setLaunchAbility(ability: WantAgent, callback: AsyncCallback&lt;void&gt;): void | 设置启动UIAbility。 |
| getController(callback: AsyncCallback&lt;AVSessionController&gt;): void | 获取当前会话自身控制器。 |
| activate(callback: AsyncCallback&lt;void&gt;): void | 激活媒体会话。 |
| destroy(callback: AsyncCallback&lt;void&gt;): void | 销毁媒体会话。 |
## 开发步骤
音视频应用作为媒体会话提供方接入媒体会话的基本步骤如下所示:
1. 通过AVSessionManager的方法创建并激活媒体会话。
```ts
import AVSessionManager from '@ohos.multimedia.avsession'; //导入AVSession模块
// 创建session
async createSession() {
let session: AVSessionManager.AVSession = await AVSessionManager.createAVSession(this.context, 'SESSION_NAME', 'audio');
session.activate();
console.info(`session create done : sessionId : ${session.sessionId}`);
}
```
2. 跟随媒体信息的变化,及时设置媒体会话信息。需要设置的媒体会话信息主要包括:
- 媒体会话元数据AVMetadata。
- 媒体播放状态AVPlaybackState。
音视频应用设置的媒体会话信息,会被媒体会话控制方通过AVSessionController相关方法获取后进行显示或处理。
```ts
async setSessionInfo() {
// 假设已经创建了一个session,如何创建session可以参考之前的案例
let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION;
// 播放器逻辑··· 引发媒体信息与播放状态的变更
// 设置必要的媒体信息
let metadata: AVSessionManager.AVMetadata = {
assetId: "0",
title: "TITLE",
artist: "ARTIST"
};
session.setAVMetadata(metadata).then(() => {
console.info('SetAVMetadata successfully');
}).catch((err) => {
console.info(`SetAVMetadata BusinessError: code: ${err.code}, message: ${err.message}`);
});
// 简单设置一个播放状态 - 暂停 未收藏
let playbackState: AVSessionManager.AVPlaybackState = {
state:AVSessionManager.PlaybackState.PLAYBACK_STATE_PAUSE,
isFavorite:false
};
session.setAVPlaybackState(playbackState, function (err) {
if (err) {
console.info(`SetAVPlaybackState BusinessError: code: ${err.code}, message: ${err.message}`);
} else {
console.info('SetAVPlaybackState successfully');
}
});
}
```
3. 设置用于被媒体会话控制方拉起的UIAbility。当用户操作媒体会话控制方的界面时,例如点击播控中心的卡片,可以拉起此处配置的UIAbility。
设置UIAbility时通过WantAgent接口实现,更多关于WantAgent的信息请参考[WantAgent](../reference/apis/js-apis-wantAgent.md)
```ts
import WantAgent from "@ohos.wantAgent";
```
```ts
// 假设已经创建了一个session,如何创建session可以参考之前的案例
let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION;
let wantAgentInfo: {
wants: [
{
bundleName: "com.example.musicdemo",
abilityName: "com.example.musicdemo.MainAbility"
}
],
operationType: WantAgent.OperationType.START_ABILITIES,
requestCode: 0,
wantAgentFlags: [WantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
}
WantAgent.getWantAgent(wantAgentInfo).then((agent) => {
session.setLaunchAbility(agent)
})
```
4. 注册播控命令事件监听,便于响应用户通过媒体会话控制方,例如播控中心,下发的播控命令。
> **说明:**
>
> 媒体会话提供方在注册相关播控命令事件监听时,监听的事件会在媒体会话控制方的getValidCommands()方法中体现,即媒体会话控制方会认为对应的方法有效,进而根据需要触发相应的事件。为了保证媒体会话控制方下发的播控命令可以被正常执行,媒体会话提供方请勿进行无逻辑的空实现监听。
```ts
async setListenerForMesFromController() {
// 假设已经创建了一个session,如何创建session可以参考之前的案例
let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION;
// 一般在监听器中会对播放器做相应逻辑处理
// 不要忘记处理完后需要通过set接口同步播放相关信息,参考上面的用例
session.on('play', () => {
console.info('on play , do play task');
// do some tasks ···
});
session.on('pause', () => {
console.info('on pause , do pause task');
// do some tasks ···
});
session.on('stop', () => {
console.info('on stop , do stop task');
// do some tasks ···
});
session.on('playNext', () => {
console.info('on playNext , do playNext task');
// do some tasks ···
});
session.on('playPrevious', () => {
console.info('on playPrevious , do playPrevious task');
// do some tasks ···
});
}
```
5. 获取当前媒体会话自身的控制器,与媒体会话对应进行通信交互。
```ts
async createControllerFromSession() {
// 假设已经创建了一个session,如何创建session可以参考之前的案例
let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION;
// 通过已有session获取一个controller对象
let controller: AVSessionManager.AVSessionController = await session.getController();
// controller可以与原session对象进行基本的通信交互,比如下发播放命令
let avCommand: AVSessionManager.AVControlCommand = {command:'play'};
controller.sendControlCommand(avCommand);
// 或者做状态变更监听
controller.on('playbackStateChange', 'all', (state: AVSessionManager.AVPlaybackState) => {
// do some things
});
// controller可以做的操作还有很多,具体可以参考媒体会话控制方相关的说明
}
```
6. 音视频应用在退出,并且不需要继续播放时,及时取消监听以及销毁媒体会话释放资源。
取消播控命令监听的示例代码如下所示 :
```ts
async unregisterSessionListener() {
// 假设已经创建了一个session,如何创建session可以参考之前的案例
let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION;
// 取消指定session下的相关监听
session.off('play');
session.off('pause');
session.off('stop');
session.off('playNext');
session.off('playPrevious');
}
```
销毁媒体会话示例代码如下所示:
```ts
async destroySession() {
// 假设已经创建了一个session,如何创建session可以参考之前的案例
let session: AVSessionManager.AVSession = ALLREADY_CREATE_A_SESSION;
// 主动销毁已创建的session
session.destroy(function (err) {
if (err) {
console.info(`Destroy BusinessError: code: ${err.code}, message: ${err.message}`);
} else {
console.info('Destroy : SUCCESS ');
}
});
}
```
# 使用分布式媒体会话
## 基本概念
- 远端媒体会话:可信组网内的设备创建本地媒体会话后,媒体会话服务会将本地会话自动同步到远端,生成远端媒体会话。本地媒体会话与远端媒体会话时刻保持同步。
- 远端媒体会话控制器:远端投播后,远端设备上的媒体会话控制器。
## 接口说明
使用分布式媒体会话进行远端投播时使用的关键接口如下表所示。接口返回值有两种返回形式:callback和promise,下表中为callback形式接口,promise和callback只是返回值方式不一样,功能相同。
更多API说明请参见[API文档](../reference/apis/js-apis-avsession.md)
| 接口名 | 说明 |
| -------- | -------- |
| castAudio(session: SessionToken \| ‘all’, audioDevices: Array&lt;audio.AudioDeviceDescriptor&gt;, callback: AsyncCallback&lt;void&gt;): void | 投播会话到指定设备列表。 |
## 开发步骤
系统应用作为媒体会话控制方接入媒体会话时,根据需要使用分布式媒体会话进行投播的步骤如下所示:
1. 导入模块接口。由于在进行投播之前,需要从audio模块获取音频设备描述符AudioDeviceDescriptor,所以除了导入avsession模块外,还需要导入audio模块。
```ts
import AVSessionManager from '@ohos.multimedia.avsession';
import audio from '@ohos.multimedia.audio';
```
2. 通过AVSessionManager的castAudio接口将当前设备所有会话投播到其他设备。
```ts
// 投播到其他设备
let audioManager = audio.getAudioManager();
let audioRoutingManager = audioManager.getRoutingManager();
let audioDevices;
await audioRoutingManager.getDevices(audio.DeviceFlag.OUTPUT_DEVICES_FLAG).then((data) => {
audioDevices = data;
console.info('Promise returned to indicate that the device list is obtained.');
}).catch((err) => {
console.info(`getDevices : ERROR : ${err.message}`);
});
AVSessionManager.castAudio('all', audioDevices).then(() => {
console.info('createController : SUCCESS');
}).catch((err) => {
console.info(`createController : ERROR : ${err.message}`);
});
```
系统应用在投播主控端发起投播后,媒体会话框架会通知远端设备的AVSession服务创建远端媒体会话。投播主控端的媒体会话变化时(例如媒体信息变化、播放状态变化等),媒体会话框架会自动同步变化到远端设备。
远端设备上的媒体会话处理机制,与本地设备上的机制保持一致,即远端设备上的媒体会话控制方,例如播控中心,会监听媒体会话创建事件,并创建相应的远端媒体会话控制器对远端媒体会话进行管理,而管理过程中的控制命令会由媒体会话框架自动同步到投播主控端设备。
本地投播主控端中的媒体会话提供方,例如各音视频应用,会监听各类播控命令事件,从而可以及时响应来自远端设备的各类播控命令。
# 使用OpenSL ES开发音频播放功能
OpenSL ES全称为Open Sound Library for Embedded Systems,是一个嵌入式、跨平台、免费的音频处理库。为嵌入式移动多媒体设备上的应用开发者提供标准化、高性能、低延迟的API。OpenHarmony的Native API基于[Khronos Group](https://www.khronos.org/)开发的[OpenSL ES](https://www.khronos.org/opensles/) 1.0.1 API 规范实现,开发者可以通过&lt;OpenSLES.h&gt;&lt;OpenSLES_OpenHarmony.h&gt;在OpenHarmony上使用相关API。
## OpenHarmony上的OpenSL ES
OpenSL ES中提供了以下的接口,OpenHarmony当前仅实现了部分[接口](https://gitee.com/openharmony/third_party_opensles/blob/master/api/1.0.1/OpenSLES.h),可以实现音频播放的基础功能。
调用未实现接口后会返回**SL_RESULT_FEATURE_UNSUPPORTED,**当前没有相关扩展可以使用。
以下列表列举了OpenHarmony上已实现的OpenSL ES的接口,具体说明请参考[OpenSL ES](https://www.khronos.org/opensles/)规范:
- **OpenHarmony上支持的Engine接口:**
- SLresult (\*CreateAudioPlayer) (SLEngineItf self, SLObjectItf \* pPlayer, SLDataSource \*pAudioSrc, SLDataSink \*pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired)
- SLresult (\*CreateAudioRecorder) (SLEngineItf self, SLObjectItf \* pRecorder, SLDataSource \*pAudioSrc, SLDataSink \*pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired)
- SLresult (\*CreateOutputMix) (SLEngineItf self, SLObjectItf \* pMix, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired)
- **OpenHarmony上支持的Object接口:**
- SLresult (\*Realize) (SLObjectItf self, SLboolean async)
- SLresult (\*GetState) (SLObjectItf self, SLuint32 \* pState)
- SLresult (\*GetInterface) (SLObjectItf self, const SLInterfaceID iid, void \* pInterface)
- void (\*Destroy) (SLObjectItf self)
- **OpenHarmony上支持的Playback接口:**
- SLresult (\*SetPlayState) (SLPlayItf self, SLuint32 state)
- SLresult (\*GetPlayState) (SLPlayItf self, SLuint32 \*pState)
- **OpenHarmony上支持的Volume控制接口**
- SLresult (\*SetVolumeLevel) (SLVolumeItf self, SLmillibel level)
- SLresult (\*GetVolumeLevel) (SLVolumeItf self, SLmillibel \*pLevel)
- SLresult (\*GetMaxVolumeLevel) (SLVolumeItf self, SLmillibel \*pMaxLevel)
- **OpenHarmony上支持的BufferQueue接口**
以下接口需引入&lt;OpenSLES_OpenHarmony.h&gt;使用。
| 接口 | 说明 |
| -------- | -------- |
| SLresult (\*Enqueue) (SLOHBufferQueueItf self, const void \*buffer, SLuint32 size) | 根据情况将buffer加到相应队列中。<br/>如果是播放操作,则将带有音频数据的buffer插入到filledBufferQ_队列中;如果是录音操作,则将录音使用后的空闲buffer插入到freeBufferQ_队列中。<br/>self:表示调用该函数的BufferQueue接口对象。<br/>buffer:播放时表示带有音频数据的buffer,录音时表示已存储完录音数据后的空闲buffer。<br/>size:表示buffer的大小。 |
| SLresult (\*Clear) (SLOHBufferQueueItf self) | 释放BufferQueue接口对象。<br/>self:表示调用该函数的BufferQueue接口对象将被释放。 |
| SLresult (\*GetState) (SLOHBufferQueueItf self, SLOHBufferQueueState \*state) | 获取BufferQueue接口对象状态。<br/>self:表示调用该函数的BufferQueue接口对象。<br/>state:BufferQueue的当前状态。 |
| SLresult (\*RegisterCallback) (SLOHBufferQueueItf self, SlOHBufferQueueCallback callback, void\* pContext) | 注册回调函数。<br/>self:表示调用该函数的BufferQueue接口对象。<br/>callback:播放/录音时注册的回调函数。<br/>pContext:播放时传入待播放音频文件,录音时传入将要录制的音频文件。 |
| SLresult (\*GetBuffer) (SLOHBufferQueueItf self, SLuint8\*\* buffer, SLuint32\* size) | 根据情况获取相应的buffer。<br/>如果是播放操作,则从freeBufferQ_队列中获取空闲buffer;如果是录音操作,则从filledBufferQ_队列中获取携带录音数据的buffer。<br/>self:表示调用该函数的BufferQueue接口对象。<br/>buffer:播放时表示空闲的buffer,录音时表示携带录音数据的buffer。<br/>size:表示buffer的大小。 |
## 完整示例
参考以下示例代码,播放一个音频文件。
1. 添加头文件。
```c++
#include <OpenSLES.h>
#include <OpenSLES_OpenHarmony.h>
#include <OpenSLES_Platform.h>
```
2. 使用slCreateEngine接口和获取engine实例。
```c++
SLObjectItf engineObject = nullptr;
slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
```
3. 获取接口SL_IID_ENGINE的engineEngine实例。
```c++
SLEngineItf engineEngine = nullptr;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
```
4. 配置播放器信息,创建AudioPlayer。
```c++
SLDataLocator_BufferQueue slBufferQueue = {
SL_DATALOCATOR_BUFFERQUEUE,
0
};
// 具体参数需要根据音频文件格式进行适配
SLDataFormat_PCM pcmFormat = {
SL_DATAFORMAT_PCM,
2, // 通道数
SL_SAMPLINGRATE_48, // 采样率
SL_PCMSAMPLEFORMAT_FIXED_16, // 音频采样格式
0,
0,
0
};
SLDataSource slSource = {&slBufferQueue, &pcmFormat};
SLObjectItf pcmPlayerObject = nullptr;
(*engineEngine)->CreateAudioPlayer(engineEngine, &pcmPlayerObject, &slSource, null, 0, nullptr, nullptr);
(*pcmPlayerObject)->Realize(pcmPlayerObject, SL_BOOLEAN_FALSE);
```
5. 获取接口SL_IID_OH_BUFFERQUEUE的bufferQueueItf实例。
```c++
SLOHBufferQueueItf bufferQueueItf;
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_OH_BUFFERQUEUE, &bufferQueueItf);
```
6. 打开音频文件,注册BufferQueueCallback回调。
```c++
static void BufferQueueCallback (SLOHBufferQueueItf bufferQueueItf, void *pContext, SLuint32 size)
{
SLuint8 *buffer = nullptr;
SLuint32 pSize;
(*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, &pSize);
// 将待播放音频数据写入buffer
(*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, size);
}
void *pContext; // 可传入自定义的上下文信息,会在Callback内收到
(*bufferQueueItf)->RegisterCallback(bufferQueueItf, BufferQueueCallback, pContext);
```
7. 获取接口SL_PLAYSTATE_PLAYING的playItf实例,开始播放。
```c++
SLPlayItf playItf = nullptr;
(*pcmPlayerObject)->GetInterface(pcmPlayerObject, SL_IID_PLAY, &playItf);
(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_PLAYING);
```
8. 结束音频播放。
```c++
(*playItf)->SetPlayState(playItf, SL_PLAYSTATE_STOPPED);
(*pcmPlayerObject)->Destroy(pcmPlayerObject);
(*engineObject)->Destroy(engineObject);
```
# 使用OpenSLES开发音频录制功能
OpenSL ES全称为Open Sound Library for Embedded Systems,是一个嵌入式、跨平台、免费的音频处理库。为嵌入式移动多媒体设备上的应用开发者提供标准化、高性能、低延迟的API。OpenHarmony的Native API基于[Khronos Group](https://www.khronos.org/)开发的[OpenSL ES](https://www.khronos.org/opensles/) 1.0.1 API 规范实现,开发者可以通过&lt;OpenSLES.h&gt;&lt;OpenSLES_OpenHarmony.h&gt;在OpenHarmony上使用相关API。
## OpenHarmony上的OpenSL ES
OpenSL ES中提供了以下的接口,OpenHarmony当前仅实现了部分[接口](https://gitee.com/openharmony/third_party_opensles/blob/master/api/1.0.1/OpenSLES.h),可以实现音频播放的基础功能。
调用未实现接口后会返回**SL_RESULT_FEATURE_UNSUPPORTED,**当前没有相关扩展可以使用。
以下列表列举了OpenHarmony上已实现的OpenSL ES的接口,具体说明请参考[OpenSL ES](https://www.khronos.org/opensles/)规范:
- **OpenHarmony上支持的Engine接口:**
- SLresult (\*CreateAudioPlayer) (SLEngineItf self, SLObjectItf \* pPlayer, SLDataSource \*pAudioSrc, SLDataSink \*pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired)
- SLresult (\*CreateAudioRecorder) (SLEngineItf self, SLObjectItf \* pRecorder, SLDataSource \*pAudioSrc, SLDataSink \*pAudioSnk, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired)
- SLresult (\*CreateOutputMix) (SLEngineItf self, SLObjectItf \* pMix, SLuint32 numInterfaces, const SLInterfaceID \* pInterfaceIds, const SLboolean \* pInterfaceRequired)
- **OpenHarmony上支持的Object接口:**
- SLresult (\*Realize) (SLObjectItf self, SLboolean async)
- SLresult (\*GetState) (SLObjectItf self, SLuint32 \* pState)
- SLresult (\*GetInterface) (SLObjectItf self, const SLInterfaceID iid, void \* pInterface)
- void (\*Destroy) (SLObjectItf self)
- **OpenHarmony上支持的Recorder接口:**
- SLresult (\*SetRecordState) (SLRecordItf self, SLuint32 state)
- SLresult (\*GetRecordState) (SLRecordItf self,SLuint32 \*pState)
- **OpenHarmony上支持的BufferQueue接口**
以下接口需引入&lt;OpenSLES_OpenHarmony.h&gt;使用。
| 接口 | 说明 |
| -------- | -------- |
| SLresult (\*Enqueue) (SLOHBufferQueueItf self, const void \*buffer, SLuint32 size) | 根据情况将buffer加到相应队列中。<br/>如果是播放操作,则将带有音频数据的buffer插入到filledBufferQ_队列中;如果是录音操作,则将录音使用后的空闲buffer插入到freeBufferQ_队列中。<br/>self:表示调用该函数的BufferQueue接口对象。<br/>buffer:播放时表示带有音频数据的buffer,录音时表示已存储完录音数据后的空闲buffer。<br/>size:表示buffer的大小。 |
| SLresult (\*Clear) (SLOHBufferQueueItf self) | 释放BufferQueue接口对象。<br/>self:表示调用该函数的BufferQueue接口对象将被释放。 |
| SLresult (\*GetState) (SLOHBufferQueueItf self, SLOHBufferQueueState \*state) | 获取BufferQueue接口对象状态。<br/>self:表示调用该函数的BufferQueue接口对象。<br/>state:BufferQueue的当前状态。 |
| SLresult (\*RegisterCallback) (SLOHBufferQueueItf self, SlOHBufferQueueCallback callback, void\* pContext) | 注册回调函数。<br/>self:表示调用该函数的BufferQueue接口对象。<br/>callback:播放/录音时注册的回调函数。<br/>pContext:播放时传入待播放音频文件,录音时传入将要录制的音频文件。 |
| SLresult (\*GetBuffer) (SLOHBufferQueueItf self, SLuint8\*\* buffer, SLuint32\* size) | 根据情况获取相应的buffer。<br/>如果是播放操作,则从freeBufferQ_队列中获取空闲buffer;如果是录音操作,则从filledBufferQ_队列中获取携带录音数据的buffer。<br/>self:表示调用该函数的BufferQueue接口对象。<br/>buffer:播放时表示空闲的buffer,录音时表示携带录音数据的buffer。<br/>size:表示buffer的大小。 |
## 完整示例
参考下列示例代码,完成音频录制。
1. 添加头文件
```c++
#include <OpenSLES.h>
#include <OpenSLES_OpenHarmony.h>
#include <OpenSLES_Platform.h>
```
2. 使用slCreateEngine接口创建引擎对象和实例化引擎对象engine。
```c++
SLObjectItf engineObject = nullptr;
slCreateEngine(&engineObject, 0, nullptr, 0, nullptr, nullptr);
(*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
```
3. 获取接口SL_IID_ENGINE的引擎接口engineEngine实例。
```c++
SLEngineItf engineItf = nullptr;
(*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineItf);
```
4. 配置录音器信息(配置输入源audiosource、输出源audiosink),创建录音对象pcmCapturerObject。
```c++
SLDataLocator_IODevice io_device = {
SL_DATALOCATOR_IODEVICE,
SL_IODEVICE_AUDIOINPUT,
SL_DEFAULTDEVICEID_AUDIOINPUT,
NULL
};
SLDataSource audioSource = {
&io_device,
NULL
};
SLDataLocator_BufferQueue buffer_queue = {
SL_DATALOCATOR_BUFFERQUEUE,
3
};
// 具体参数需要根据音频文件格式进行适配
SLDataFormat_PCM format_pcm = {
SL_DATAFORMAT_PCM, // 输入的音频格式
1, // 单声道
SL_SAMPLINGRATE_44_1, // 采样率: 44100HZ
SL_PCMSAMPLEFORMAT_FIXED_16, // 音频采样格式,小尾数,带符号的16位整数
0,
0,
0
};
SLDataSink audioSink = {
&buffer_queue,
&format_pcm
};
SLObjectItf pcmCapturerObject = nullptr;
(*engineItf)->CreateAudioRecorder(engineItf, &pcmCapturerObject,
&audioSource, &audioSink, 0, nullptr, nullptr);
(*pcmCapturerObject)->Realize(pcmCapturerObject, SL_BOOLEAN_FALSE);
```
5. 获取录音接口SL_IID_RECORD的recordItf接口实例。
```c++
SLRecordItf recordItf;
(*pcmCapturerObject)->GetInterface(pcmCapturerObject, SL_IID_RECORD, &recordItf);
```
6. 获取接口 SL_IID_OH_BUFFERQUEUE 的 bufferQueueItf 实例
```c++
SLOHBufferQueueItf bufferQueueItf;
(*pcmCapturerObject)->GetInterface(pcmCapturerObject, SL_IID_OH_BUFFERQUEUE, &bufferQueueItf);
```
7. 注册BufferQueueCallback回调。
```c++
static void BufferQueueCallback(SLOHBufferQueueItf bufferQueueItf, void *pContext, SLuint32 size)
{
// 可从pContext获取注册时传入的使用者信息
SLuint8 *buffer = nullptr;
SLuint32 pSize = 0;
(*bufferQueueItf)->GetBuffer(bufferQueueItf, &buffer, &pSize);
if (buffer != nullptr) {
// 可从buffer内读取录音数据进行后续处理
(*bufferQueueItf)->Enqueue(bufferQueueItf, buffer, size);
}
}
void *pContext; // 可传入自定义的上下文信息,会在Callback内收到
(*bufferQueueItf)->RegisterCallback(bufferQueueItf, BufferQueueCallback, pContext);
```
8. 开始录音。
```c++
(*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_RECORDING);
```
9. 结束音频录制。
```c++
(*recordItf)->SetRecordState(recordItf, SL_RECORDSTATE_STOPPED);
(*pcmCapturerObject)->Destroy(pcmCapturerObject);
```
# 使用TonePlayer开发音频播放功能(仅对系统应用开放)
TonePlayer<sup>9+</sup>提供播放和管理DTMF(Dual Tone Multi Frequency,双音多频)音调的方法,包括各种系统监听音调、专有音调,如拨号音、通话回铃音等。主要工作是将需要生成音调的[ToneType](../reference/apis/js-apis-audio.md#tonetype9)类型,通过自带算法生成多个不同频率的正弦波叠加形成声音数据,通过[AudioRenderer](../reference/apis/js-apis-audio.md#audiorenderer8)进行播放,同时对播放任务进行管理。包含加载DTMF音调配置、启动DTMF音调播放、停止当前正在播放的音调、释放与此TonePlayer对象关联的资源等流程。详细API说明请参考[TonePlayer API文档](../reference/apis/js-apis-audio.md#toneplayer9)
## 支持的播放音调类型
播放音调类型[ToneType](../reference/apis/js-apis-audio.md#tonetype9)信息(如下表所示),可通过"audio.ToneType.指定类型" 作为参数调用load()方法加载指定类型的音调资源。
| 播放音调类型 | 值 | 说明 |
| -------- | -------- | -------- |
| TONE_TYPE_DIAL_0 | 0 | 键0的DTMF音。 |
| TONE_TYPE_DIAL_1 | 1 | 键1的DTMF音。 |
| TONE_TYPE_DIAL_2 | 2 | 键2的DTMF音。 |
| TONE_TYPE_DIAL_3 | 3 | 键3的DTMF音。 |
| TONE_TYPE_DIAL_4 | 4 | 键4的DTMF音。 |
| TONE_TYPE_DIAL_5 | 5 | 键5的DTMF音。 |
| TONE_TYPE_DIAL_6 | 6 | 键6的DTMF音。 |
| TONE_TYPE_DIAL_7 | 7 | 键7的DTMF音。 |
| TONE_TYPE_DIAL_8 | 8 | 键8的DTMF音。 |
| TONE_TYPE_DIAL_9 | 9 | 键9的DTMF音。 |
| TONE_TYPE_DIAL_S | 10 | 键\*的DTMF音。 |
| TONE_TYPE_DIAL_P | 11 | 键\#的DTMF音。 |
| TONE_TYPE_DIAL_A | 12 | 键A的DTMF音。 |
| TONE_TYPE_DIAL_B | 13 | 键B的DTMF音。 |
| TONE_TYPE_DIAL_C | 14 | 键C的DTMF音。 |
| TONE_TYPE_DIAL_D | 15 | 键D的DTMF音。 |
| TONE_TYPE_COMMON_SUPERVISORY_DIAL | 100 | 呼叫监管音调,拨号音。 |
| TONE_TYPE_COMMON_SUPERVISORY_BUSY | 101 | 呼叫监管音调,忙。 |
| TONE_TYPE_COMMON_SUPERVISORY_CONGESTION | 102 | 呼叫监管音调,拥塞。 |
| TONE_TYPE_COMMON_SUPERVISORY_RADIO_ACK | 103 | 呼叫监管音调,无线电&nbsp;ACK。 |
| TONE_TYPE_COMMON_SUPERVISORY_RADIO_NOT_AVAILABLE | 104 | 呼叫监管音调,无线电不可用。 |
| TONE_TYPE_COMMON_SUPERVISORY_CALL_WAITING | 106 | 呼叫监管音调,呼叫等待。 |
| TONE_TYPE_COMMON_SUPERVISORY_RINGTONE | 107 | 呼叫监管音调,铃声。 |
| TONE_TYPE_COMMON_PROPRIETARY_BEEP | 200 | 专有声调,一般蜂鸣声。 |
| TONE_TYPE_COMMON_PROPRIETARY_ACK | 201 | 专有声调,ACK。 |
| TONE_TYPE_COMMON_PROPRIETARY_PROMPT | 203 | 专有声调,PROMPT。 |
| TONE_TYPE_COMMON_PROPRIETARY_DOUBLE_BEEP | 204 | 专有声调,双重蜂鸣声。 |
## 开发步骤及注意事项
以下步骤描述了TonePlayer接口实现播放功能流程:
1. 创建DTMF播放器 ,获取tonePlayer实例。
```ts
import audio from '@ohos.multimedia.audio';
let audioRendererInfo = {
content : audio.ContentType.CONTENT_TYPE_SONIFICATION,
usage : audio.StreamUsage.STREAM_USAGE_MEDIA,
rendererFlags : 0
};
tonePlayerPromise = audio.createTonePlayer(audioRendererInfo);
```
2. 加载指定类型DTMF音调配置。
```ts
tonePlayerPromise.load(audio.ToneType.TONE_TYPE_DIAL_0);
```
3. 启动DTMF音调播放。
```ts
tonePlayerPromise.start();
```
4. 停止当前正在播放的音调。
```ts
tonePlayerPromise.stop();
```
5. 释放与此TonePlayer对象关联的资源。
```ts
tonePlayerPromise.release();
```
在接口未按此正常调用时序调用时,接口会返回错误码6800301 NAPI_ERR_SYSTEM。
## 完整示例
参考以下示例,点击键盘拨号按键,并启动对应的DTMF音调播放。
为保证UI线程不被阻塞,大部分TonePlayer调用都是异步的。对于每个API均提供了callback函数和Promise函数,以下示例均采用Promise函数,更多方式可参考API文档[TonePlayer](../reference/apis/js-apis-audio.md#toneplayer9)
```ts
import audio from '@ohos.multimedia.audio';
export class TonelayerDemo {
private timer : number;
private timerPro : number;
// promise调用方式
async testTonePlayerPromise(type) {
console.info('testTonePlayerPromise start');
if (this.timerPro) clearTimeout(this.timerPro);
let tonePlayerPromise;
let audioRendererInfo = {
content : audio.ContentType.CONTENT_TYPE_SONIFICATION,
usage : audio.StreamUsage.STREAM_USAGE_MEDIA,
rendererFlags : 0
};
this.timerPro = setTimeout(async () => {
try {
console.info('testTonePlayerPromise: createTonePlayer');
// 创建DTMF播放器
tonePlayerPromise = await audio.createTonePlayer(audioRendererInfo);
console.info('testTonePlayerPromise: createTonePlayer-success');
console.info(`testTonePlayerPromise: load type: ${type}`);
// 加载type类型音调
await tonePlayerPromise.load(type);
console.info('testTonePlayerPromise: load-success');
console.info(`testTonePlayerPromise: start type: ${type}`);
// 启动DTMF音调播放
await tonePlayerPromise.start();
console.info('testTonePlayerPromise: start-success');
console.info(`testTonePlayerPromise: stop type: ${type}`);
setTimeout(async()=>{
// 停止当前正在播放的音调
await tonePlayerPromise.stop();
console.info('testTonePlayerPromise: stop-success');
console.info(`testTonePlayerPromise: release type: ${type}`);
// 释放与此TonePlayer对象关联的资源
await tonePlayerPromise.release();
console.info('testTonePlayerPromise: release-success');
}, 30)
} catch(err) {
console.error(`testTonePlayerPromise err : ${err}`);
}
}, 200)
};
async testTonePlayer() {
this.testTonePlayerPromise(audio.ToneType.TONE_TYPE_DIAL_0);
}
}
```
# 视频播放开发指导
# 视频播放
## 简介
在OpenHarmony系统中,提供两种视频播放开发的方案:
视频播放的主要工作是将视频数据转码并输出到设备进行播放,同时管理播放任务,包括开始播放、暂停播放、停止播放、资源释放、音量设置、跳转播放位置、设置倍数、获取轨道信息等功能控制。本文将对视频播放全流程、视频切换、视频循环播放等场景开发进行介绍说明
- [AVPlayer](using-avplayer-for-playback.md):功能较完善的音视频播放ArkTS/JS API,集成了流媒体和本地资源解析,媒体资源解封装,视频解码和渲染功能,适用于对媒体资源进行端到端播放的场景,可直接播放mp4、mkv等格式的视频文件
## 运作机制
- Video组件:封装了视频播放的基础能力,需要设置数据源以及基础信息即可播放视频,但相对扩展能力较弱。Video组件由ArkUI提供能力,相关指导请参考UI开发文档-[Video组件](../ui/arkts-common-components-video-player.md)
该模块提供了视频播放状态变化示意图和视频播放外部模块交互图。
**图1** 视频播放状态变化示意图
![zh-ch_image_video_state_machine](figures/zh-ch_image_video_state_machine.png)
**图2** 视频播放外部模块交互图
![zh-ch_image_video_player](figures/zh-ch_image_video_player.png)
**说明**:三方应用通过调用JS接口层提供的js接口实现相应功能时,框架层会通过Native Framework的媒体服务,调用音频部件将软件解码后的音频数据,输出至音频HDI,和图形子系统将硬件接口层的解码HDI部件的解码后的图像数据,输出至显示HDI,实现视频播放功能。
*注意:视频播放需要显示、音频、编解码等硬件能力。*
1. 三方应用从Xcomponent组件获取surfaceID。
2. 三方应用把surfaceID传递给VideoPlayer JS。
3. 媒体服务把帧数据flush给surface buffer。
## 兼容性说明
视频播放支持的视频格式分必选规格和可选规格。必选规格为所有厂商均支持的视频格式。对于可选规格,厂商将基于实际情况决定是否实现。建议开发者做兼容处理,保证全平台兼容。
推荐使用主流的播放格式和主流分辨率,不建议开发者自制非常或者异常码流,以免产生无法播放、卡住、花屏等兼容性问题。若发生此类问题不会影响系统,退出码流播放即可。
| 视频格式 | 是否必选规格 |
|:--------:|:-----:|
| H264 | 是 |
| MPEG2 | 否 |
| MPEG4 | 否 |
| H263 | 否 |
| VP8 | 否 |
主流的播放格式和主流分辨率如下:
| 视频容器规格 | 规格描述 | 分辨率 |
| :----------: | :-----------------------------------------------: | :--------------------------------: |
| mp4 | 视频格式:H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P |
| mkv | 视频格式:H264/MPEG2/MPEG4/H263 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P |
| ts | 视频格式:H264/MPEG2/MPEG4 音频格式:AAC/MP3 | 主流分辨率,如1080P/720P/480P/270P |
| webm | 视频格式:VP8 音频格式:VORBIS | 主流分辨率,如1080P/720P/480P/270P |
本开发指导将介绍如何使用AVPlayer开发视频播放功能,以完整地播放一个视频作为示例,实现端到端播放原始媒体资源。如果要实现后台播放或熄屏播放,需要使用[AVSession(媒体会话)](avsession-overview.md)[申请长时任务](../task-management/continuous-task-dev-guide.md),避免播放过程中音频模块被系统强制中断。
## 开发指导
详细API含义可参考:[媒体服务API文档VideoPlayer](../reference/apis/js-apis-media.md#videoplayer8)
### 全流程场景
视频播放的全流程场景包含:创建实例,设置url,设置SurfaceId,准备播放视频,播放视频,暂停播放,获取轨道信息,跳转播放位置,设置音量,设置倍速,结束播放,重置,释放资源等流程。
VideoPlayer支持的url媒体源输入类型可参考:[url属性说明](../reference/apis/js-apis-media.md#videoplayer_属性)
Xcomponent创建方法可参考:[Xcomponent创建方法](../reference/arkui-ts/ts-basic-components-xcomponent.md)
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
export class VideoPlayerDemo {
// 函数调用发生错误时用于上报错误信息
failureCallback(error) {
console.info(`error happened,error Name is ${error.name}`);
console.info(`error happened,error Code is ${error.code}`);
console.info(`error happened,error Message is ${error.message}`);
}
// 当函数调用发生异常时用于上报错误信息
catchCallback(error) {
console.info(`catch error happened,error Name is ${error.name}`);
console.info(`catch error happened,error Code is ${error.code}`);
console.info(`catch error happened,error Message is ${error.message}`);
}
// 用于打印视频轨道信息
printfDescription(obj) {
for (let item in obj) {
let property = obj[item];
console.info('key is ' + item);
console.info('value is ' + property);
}
}
async videoPlayerDemo() {
let videoPlayer = undefined;
let surfaceID = 'test' // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法
let fdPath = 'fd://'
// path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile" 命令,将其推送到设备上
let path = '/data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile/H264_AAC.mp4';
let file = await fs.open(path);
fdPath = fdPath + '' + file.fd;
// 调用createVideoPlayer接口返回videoPlayer实例对象
await media.createVideoPlayer().then((video) => {
if (typeof (video) != 'undefined') {
console.info('createVideoPlayer success!');
videoPlayer = video;
} else {
console.info('createVideoPlayer fail!');
播放的全流程包含:创建AVPlayer,设置播放资源和窗口,设置播放参数(音量/倍速/缩放模式),播放控制(播放/暂停/跳转/停止),重置,销毁资源。在进行应用开发的过程中,开发者可以通过AVPlayer的state属性主动获取当前状态或使用on('stateChange')方法监听状态变化。如果应用在视频播放器处于错误状态时执行操作,系统可能会抛出异常或生成其他未定义的行为。
**图1** 播放状态变化示意图  
![Playback status change](figures/video-playback-status-change.png)
状态的详细说明请参考[AVPlayerState](../reference/apis/js-apis-media.md#avplayerstate9)。当播放处于prepared / playing / paused / compeled状态时,播放引擎处于工作状态,这需要占用系统较多的运行内存。当客户端暂时不使用播放器时,调用reset()或release()回收内存资源,做好资源利用。
### 开发步骤及注意事项
详细的API说明请参考[AVPlayer API参考](../reference/apis/js-apis-media.md#avplayer9)
1. 创建实例createAVPlayer(),AVPlayer初始化idle状态。
2. 设置业务需要的监听事件,搭配全流程场景使用。支持的监听事件包括:
| 事件类型 | 说明 |
| -------- | -------- |
| stateChange | 必要事件,监听播放器的state属性改变。 |
| error | 必要事件,监听播放器的错误信息。 |
| durationUpdate | 用于进度条,监听进度条长度,刷新资源时长。 |
| timeUpdate | 用于进度条,监听进度条当前位置,刷新当前时间。 |
| seekDone | 响应API调用,监听seek()请求完成情况。<br/>当使用seek()跳转到指定播放位置后,如果seek操作成功,将上报该事件。 |
| speedDone | 响应API调用,监听setSpeed()请求完成情况。<br/>当使用setSpeed()跳转到指定播放位置后,如果setSpeed操作成功,将上报该事件。 |
| volumeChange | 响应API调用,监听setVolume()请求完成情况。<br/>当使用setVolume()跳转到指定播放位置后,如果setVolume操作成功,将上报该事件。 |
| bitrateDone | 响应API调用,用于HLS协议流,监听setBitrate()请求完成情况。<br/>当使用setBitrate()指定播放比特率后,如果setBitrate操作成功,将上报该事件。 |
| availableBitrates | 用于HLS协议流,监听HLS资源的可选bitrates,用于setBitrate()。 |
| bufferingUpdate | 用于网络播放,监听网络播放缓冲信息。 |
| startRenderFrame | 用于视频播放,监听视频播放首帧渲染时间。 |
| videoSizeChange | 用于视频播放,监听视频播放的宽高信息,可用于调整窗口大小、比例。 |
| audioInterrupt | 监听音频焦点切换信息,搭配属性audioInterruptMode使用。<br/>如果当前设备存在多个媒体正在播放,音频焦点被切换(即播放其他媒体如通话等)时将上报该事件,应用可以及时处理。 |
3. 设置资源:设置属性url,AVPlayer进入initialized状态。
> **说明:**
>
> 下面代码示例中的url仅作示意使用,开发者需根据实际情况,确认资源有效性并设置:
>
> - 确认相应的资源文件可用,并使用应用沙箱路径访问对应资源,参考[获取应用文件路径](../application-models/application-context-stage.md#获取应用开发路径)。应用沙箱的介绍及如何向应用沙箱推送文件,请参考[文件管理](../file-management/app-sandbox-directory.md)。
>
> - 如果使用网络播放路径,需[申请相关权限](../security/accesstoken-guidelines.md):ohos.permission.INTERNET。
>
> - 如果使用ResourceManager.getRawFd打开HAP资源文件描述符,使用方法可参考[ResourceManager API参考](../reference/apis/js-apis-resource-manager.md#getrawfd9)。
>
> - 需要使用[支持的播放格式与协议](avplayer-avrecorder-overview.md#支持的格式与协议)。
4. 设置窗口:获取并设置属性SurfaceID,用于设置显示画面。
应用需要从XComponent组件获取surfaceID,获取方式请参考[XComponent](../reference/arkui-ts/ts-basic-components-xcomponent.md)
5. 准备播放:调用prepare(),AVPlayer进入prepared状态,此时可以获取duration,设置缩放模式、音量等。
6. 视频播控:播放play(),暂停pause(),跳转seek(),停止stop() 等操作。
7. 调用reset()重置资源,AVPlayer重新进入idle状态,允许更换资源url。
8. 调用release()销毁实例,AVPlayer进入released状态,退出播放。
### 完整示例
```ts
import media from '@ohos.multimedia.media';
import fs from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
export class AVPlayerDemo {
private avPlayer;
private count: number = 0;
private surfaceID: string; // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接见上面Xcomponent创建方法
// 注册avplayer回调函数
setAVPlayerCallback() {
// seek操作结果回调函数
this.avPlayer.on('seekDone', (seekDoneTime) => {
console.info(`AVPlayer seek succeeded, seek time is ${seekDoneTime}`);
})
// error回调监听函数,当avPlayer在操作过程中出现错误时调用reset接口触发重置流程
this.avPlayer.on('error', (err) => {
console.error(`Invoke avPlayer failed, code is ${err.code}, message is ${err.message}`);
this.avPlayer.reset(); // 调用reset重置资源,触发idle状态
})
// 状态机变化回调函数
this.avPlayer.on('stateChange', async (state, reason) => {
switch (state) {
case 'idle': // 成功调用reset接口后触发该状态机上报
console.info('AVPlayer state idle called.');
this.avPlayer.release(); // 调用release接口销毁实例对象
break;
case 'initialized': // avplayer 设置播放源后触发该状态上报
console.info('AVPlayerstate initialized called.');
this.avPlayer.surfaceId = this.surfaceID; // 设置显示画面,当播放的资源为纯音频时无需设置
this.avPlayer.prepare().then(() => {
console.info('AVPlayer prepare succeeded.');
}, (err) => {
console.error(`Invoke prepare failed, code is ${err.code}, message is ${err.message}`);
});
break;
case 'prepared': // prepare调用成功后上报该状态机
console.info('AVPlayer state prepared called.');
this.avPlayer.play(); // 调用播放接口开始播放
break;
case 'playing': // play成功调用后触发该状态机上报
console.info('AVPlayer state playing called.');
if (this.count !== 0) {
console.info('AVPlayer start to seek.');
this.avPlayer.seek(this.avPlayer.duration); //seek到视频末尾
} else {
this.avPlayer.pause(); // 调用暂停接口暂停播放
}
this.count++;
break;
case 'paused': // pause成功调用后触发该状态机上报
console.info('AVPlayer state paused called.');
this.avPlayer.play(); // 再次播放接口开始播放
break;
case 'completed': // 播放结束后触发该状态机上报
console.info('AVPlayer state completed called.');
this.avPlayer.stop(); //调用播放结束接口
break;
case 'stopped': // stop接口成功调用后触发该状态机上报
console.info('AVPlayer state stopped called.');
this.avPlayer.reset(); // 调用reset接口初始化avplayer状态
break;
case 'released':
console.info('AVPlayer state released called.');
break;
default:
console.info('AVPlayer state unknown called.');
break;
}
}, this.failureCallback).catch(this.catchCallback);
// 设置播放源
videoPlayer.url = fdPath;
// 设置surfaceID用于显示视频画面
await videoPlayer.setDisplaySurface(surfaceID).then(() => {
console.info('setDisplaySurface success');
}, this.failureCallback).catch(this.catchCallback);
// 调用prepare完成播放前准备工作
await videoPlayer.prepare().then(() => {
console.info('prepare success');
}, this.failureCallback).catch(this.catchCallback);
// 调用play接口正式开始播放
await videoPlayer.play().then(() => {
console.info('play success');
}, this.failureCallback).catch(this.catchCallback);
// 暂停播放
await videoPlayer.pause().then(() => {
console.info('pause success');
}, this.failureCallback).catch(this.catchCallback);
// 通过promise回调方式获取视频轨道信息communication_dsoftbus
let arrayDescription;
await videoPlayer.getTrackDescription().then((arrlist) => {
if (typeof (arrlist) != 'undefined') {
arrayDescription = arrlist;
} else {
console.log('video getTrackDescription fail');
}
}, this.failureCallback).catch(this.catchCallback);
for (let i = 0; i < arrayDescription.length; i++) {
this.printfDescription(arrayDescription[i]);
}
// 跳转播放时间到50s位置,具体入参意义请参考接口文档
let seekTime = 50000;
await videoPlayer.seek(seekTime, media.SeekMode.SEEK_NEXT_SYNC).then((seekDoneTime) => {
console.info('seek success');
}, this.failureCallback).catch(this.catchCallback);
// 音量设置接口,具体入参意义请参考接口文档
let volume = 0.5;
await videoPlayer.setVolume(volume).then(() => {
console.info('setVolume success');
}, this.failureCallback).catch(this.catchCallback);
// 倍速设置接口,具体入参意义请参考接口文档
let speed = media.PlaybackSpeed.SPEED_FORWARD_2_00_X;
await videoPlayer.setSpeed(speed).then(() => {
console.info('setSpeed success');
}, this.failureCallback).catch(this.catchCallback);
// 结束播放
await videoPlayer.stop().then(() => {
console.info('stop success');
}, this.failureCallback).catch(this.catchCallback);
// 重置播放配置
await videoPlayer.reset().then(() => {
console.info('reset success');
}, this.failureCallback).catch(this.catchCallback);
// 释放播放资源
await videoPlayer.release().then(() => {
console.info('release success');
}, this.failureCallback).catch(this.catchCallback);
// 相关对象置undefined
videoPlayer = undefined;
surfaceID = undefined;
}
}
```
### 正常播放场景
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
export class VideoPlayerDemo {
// 函数调用发生错误时用于上报错误信息
failureCallback(error) {
console.info(`error happened,error Name is ${error.name}`);
console.info(`error happened,error Code is ${error.code}`);
console.info(`error happened,error Message is ${error.message}`);
}
// 当函数调用发生异常时用于上报错误信息
catchCallback(error) {
console.info(`catch error happened,error Name is ${error.name}`);
console.info(`catch error happened,error Code is ${error.code}`);
console.info(`catch error happened,error Message is ${error.message}`);
}
// 用于打印视频轨道信息
printfDescription(obj) {
for (let item in obj) {
let property = obj[item];
console.info('key is ' + item);
console.info('value is ' + property);
}
}
async videoPlayerDemo() {
let videoPlayer = undefined;
let surfaceID = 'test' // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接:
let fdPath = 'fd://'
// path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile" 命令,将其推送到设备上
let path = '/data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile/H264_AAC.mp4';
})
}
// 以下demo为使用fs文件系统打开沙箱地址获取媒体文件地址并通过url属性进行播放示例
async avPlayerUrlDemo() {
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数
this.setAVPlayerCallback();
let fdPath = 'fd://';
let context = getContext(this) as common.UIAbilityContext;
// 通过UIAbilityContext获取沙箱地址filesDir,以Stage模型为例
let pathDir = context.filesDir;
let path = pathDir + '/H264_AAC.mp4';
// 打开相应的资源文件地址获取fd,并为url赋值触发initialized状态机上报
let file = await fs.open(path);
fdPath = fdPath + '' + file.fd;
// 调用createVideoPlayer接口返回videoPlayer实例对象
await media.createVideoPlayer().then((video) => {
if (typeof (video) != 'undefined') {
console.info('createVideoPlayer success!');
videoPlayer = video;
} else {
console.info('createVideoPlayer fail!');
}
}, this.failureCallback).catch(this.catchCallback);
// 设置播放源
videoPlayer.url = fdPath;
// 设置surfaceID用于显示视频画面
await videoPlayer.setDisplaySurface(surfaceID).then(() => {
console.info('setDisplaySurface success');
}, this.failureCallback).catch(this.catchCallback);
// 调用prepare完成播放前准备工作
await videoPlayer.prepare().then(() => {
console.info('prepare success');
}, this.failureCallback).catch(this.catchCallback);
// 调用play接口正式开始播放
await videoPlayer.play().then(() => {
console.info('play success');
}, this.failureCallback).catch(this.catchCallback);
// 结束播放
await videoPlayer.stop().then(() => {
console.info('stop success');
}, this.failureCallback).catch(this.catchCallback);
// 释放播放资源
await videoPlayer.release().then(() => {
console.info('release success');
}, this.failureCallback).catch(this.catchCallback);
// 相关对象置undefined
videoPlayer = undefined;
surfaceID = undefined;
}
}
```
### 切视频场景
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
export class VideoPlayerDemo {
// 函数调用发生错误时用于上报错误信息
failureCallback(error) {
console.info(`error happened,error Name is ${error.name}`);
console.info(`error happened,error Code is ${error.code}`);
console.info(`error happened,error Message is ${error.message}`);
}
// 当函数调用发生异常时用于上报错误信息
catchCallback(error) {
console.info(`catch error happened,error Name is ${error.name}`);
console.info(`catch error happened,error Code is ${error.code}`);
console.info(`catch error happened,error Message is ${error.message}`);
}
// 用于打印视频轨道信息
printfDescription(obj) {
for (let item in obj) {
let property = obj[item];
console.info('key is ' + item);
console.info('value is ' + property);
}
}
async videoPlayerDemo() {
let videoPlayer = undefined;
let surfaceID = 'test' // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接:
let fdPath = 'fd://'
// path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile" 命令,将其推送到设备上
let path = '/data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile/H264_AAC.mp4';
let nextPath = '/data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile/MP4_AAC.mp4';
let file = await fs.open(path);
fdPath = fdPath + '' + file.fd;
// 调用createVideoPlayer接口返回videoPlayer实例对象
await media.createVideoPlayer().then((video) => {
if (typeof (video) != 'undefined') {
console.info('createVideoPlayer success!');
videoPlayer = video;
} else {
console.info('createVideoPlayer fail!');
}
}, this.failureCallback).catch(this.catchCallback);
// 设置播放源
videoPlayer.url = fdPath;
// 设置surfaceID用于显示视频画面
await videoPlayer.setDisplaySurface(surfaceID).then(() => {
console.info('setDisplaySurface success');
}, this.failureCallback).catch(this.catchCallback);
// 调用prepare完成播放前准备工作
await videoPlayer.prepare().then(() => {
console.info('prepare success');
}, this.failureCallback).catch(this.catchCallback);
// 调用play接口正式开始播放
await videoPlayer.play().then(() => {
console.info('play success');
}, this.failureCallback).catch(this.catchCallback);
// 重置播放配置
await videoPlayer.reset().then(() => {
console.info('reset success');
}, this.failureCallback).catch(this.catchCallback);
// 获取下一个视频fd地址
fdPath = 'fd://'
let nextFile = await fs.open(nextPath);
fdPath = fdPath + '' + nextFile.fd;
// 设置第二个视频播放源
videoPlayer.url = fdPath;
// 调用prepare完成播放前准备工作
await videoPlayer.prepare().then(() => {
console.info('prepare success');
}, this.failureCallback).catch(this.catchCallback);
// 调用play接口正式开始播放
await videoPlayer.play().then(() => {
console.info('play success');
}, this.failureCallback).catch(this.catchCallback);
// 释放播放资源
await videoPlayer.release().then(() => {
console.info('release success');
}, this.failureCallback).catch(this.catchCallback);
// 相关对象置undefined
videoPlayer = undefined;
surfaceID = undefined;
}
}
```
### 单个视频循环场景
```js
import media from '@ohos.multimedia.media'
import fs from '@ohos.file.fs'
export class VideoPlayerDemo {
// 函数调用发生错误时用于上报错误信息
failureCallback(error) {
console.info(`error happened,error Name is ${error.name}`);
console.info(`error happened,error Code is ${error.code}`);
console.info(`error happened,error Message is ${error.message}`);
}
// 当函数调用发生异常时用于上报错误信息
catchCallback(error) {
console.info(`catch error happened,error Name is ${error.name}`);
console.info(`catch error happened,error Code is ${error.code}`);
console.info(`catch error happened,error Message is ${error.message}`);
}
// 用于打印视频轨道信息
printfDescription(obj) {
for (let item in obj) {
let property = obj[item];
console.info('key is ' + item);
console.info('value is ' + property);
}
}
async videoPlayerDemo() {
let videoPlayer = undefined;
let surfaceID = 'test' // surfaceID用于播放画面显示,具体的值需要通过Xcomponent接口获取,相关文档链接:
let fdPath = 'fd://'
// path路径的码流可通过"hdc file send D:\xxx\H264_AAC.mp4 /data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile" 命令,将其推送到设备上
let path = '/data/app/el1/bundle/public/ohos.acts.multimedia.video.videoplayer/ohos.acts.multimedia.video.videoplayer/assets/entry/resources/rawfile/H264_AAC.mp4';
let file = await fs.open(path);
fdPath = fdPath + '' + file.fd;
// 调用createVideoPlayer接口返回videoPlayer实例对象
await media.createVideoPlayer().then((video) => {
if (typeof (video) != 'undefined') {
console.info('createVideoPlayer success!');
videoPlayer = video;
} else {
console.info('createVideoPlayer fail!');
}
}, this.failureCallback).catch(this.catchCallback);
// 设置播放源
videoPlayer.url = fdPath;
// 设置surfaceID用于显示视频画面
await videoPlayer.setDisplaySurface(surfaceID).then(() => {
console.info('setDisplaySurface success');
}, this.failureCallback).catch(this.catchCallback);
// 调用prepare完成播放前准备工作
await videoPlayer.prepare().then(() => {
console.info('prepare success');
}, this.failureCallback).catch(this.catchCallback);
// 设置循环播放属性
videoPlayer.loop = true;
// 调用play接口正式开始循环播放
await videoPlayer.play().then(() => {
console.info('play success, loop value is ' + videoPlayer.loop);
}, this.failureCallback).catch(this.catchCallback);
this.avPlayer.url = fdPath;
}
// 以下demo为使用资源管理接口获取打包在HAP内的媒体资源文件并通过fdSrc属性进行播放示例
async avPlayerFdSrcDemo() {
// 创建avPlayer实例对象
this.avPlayer = await media.createAVPlayer();
// 创建状态机变化回调函数
this.setAVPlayerCallback();
// 通过UIAbilityContext的resourceManager成员的getRawFd接口获取媒体资源播放地址
// 返回类型为{fd,offset,length},fd为HAP包fd地址,offset为媒体资源偏移量,length为播放长度
let context = getContext(this) as common.UIAbilityContext;
let fileDescriptor = await context.resourceManager.getRawFd('H264_AAC.mp4');
// 为fdSrc赋值触发initialized状态机上报
this.avPlayer.fdSrc = fileDescriptor;
}
}
```
## 相关实例
针对视频播放开发,有以下相关实例可供参考:
- [`MediaCollections:`媒体管理合集(ArkTS)(API9)(Full SDK)](https://gitee.com/openharmony/applications_app_samples/tree/master/code/BasicFeature/FileManagement/MediaCollections)
- [视频播放器(ArkTS)(Full SDK)(API9)](https://gitee.com/openharmony/codelabs/tree/master/Media/VideoPlayerStage)
\ No newline at end of file
# 视频录制开发指导
## 简介
视频录制的主要工作是捕获音视频信号,完成音视频编码并保存到文件中,帮助开发者轻松实现音视频录制功能,包括开始录制、暂停录制、恢复录制、停止录制、释放资源等功能控制。它允许调用者指定录制的编码格式、封装格式、文件路径等参数。
## 运作机制
该模块提供了视频录制状态变化示意图和视频录制外部模块交互图。
**图1** 视频录制状态变化示意图
![zh-ch_image_video_recorder_state_machine](figures/zh-ch_image_video_recorder_state_machine.png)
**图2** 视频录制外部模块交互图
![zh-ch_image_video_recorder_zero](figures/zh-ch_image_video_recorder_zero.png)
**说明**:三方相机应用或系统相机通过调用JS接口层提供的js接口实现相应功能时,框架层会通过Native Framework的媒体服务,调用音频部件通过音频HDI捕获的音频数据,再通过软件编码输出编码封装后的音频数据保存至文件中,和图形子系统通过视频HDI捕获的图像数据,再通过视频编码HDI编码,将编码后的图像数据保存至文件中,实现视频录制功能。
## 约束与限制
开发者在进行录制功能开发前,需要先对所开发的应用配置麦克风权限(ohos.permission.MICROPHONE)和相机权限(ohos.permission.CAMERA),权限配置相关内容可参考:[访问控制权限申请指导](../security/accesstoken-guidelines.md)
## 开发指导
详细API含义可参考:[媒体服务API文档VideoRecorder](../reference/apis/js-apis-media.md#videorecorder9)
### 全流程场景
视频录制全流程场景包含:创建实例、设置录制参数、开始录制、暂停录制、恢复录制、停止录制、释放资源等流程。
```js
import media from '@ohos.multimedia.media'
import mediaLibrary from '@ohos.multimedia.mediaLibrary'
export class VideoRecorderDemo {
private testFdNumber; // 用于保存fd地址
// pathName是传入的录制文件名,例如:01.mp4,生成后的文件地址:/storage/media/100/local/files/Video/01.mp4
// 使用mediaLibrary需要添加以下权限, ohos.permission.MEDIA_LOCATION、ohos.permission.WRITE_MEDIA、ohos.permission.READ_MEDIA
async getFd(pathName) {
let displayName = pathName;
const mediaTest = mediaLibrary.getMediaLibrary();
let fileKeyObj = mediaLibrary.FileKey;
let mediaType = mediaLibrary.MediaType.VIDEO;
let publicPath = await mediaTest.getPublicDirectory(mediaLibrary.DirectoryType.DIR_VIDEO);
let dataUri = await mediaTest.createAsset(mediaType, displayName, publicPath);
if (dataUri != undefined) {
let args = dataUri.id.toString();
let fetchOp = {
selections : fileKeyObj.ID + "=?",
selectionArgs : [args],
}
let fetchFileResult = await mediaTest.getFileAssets(fetchOp);
let fileAsset = await fetchFileResult.getAllObject();
let fdNumber = await fileAsset[0].open('Rw');
this.testFdNumber = "fd://" + fdNumber.toString();
}
}
// 当发生错误上上报的错误回调接口
failureCallback(error) {
console.info('error happened, error name is ' + error.name);
console.info('error happened, error code is ' + error.code);
console.info('error happened, error message is ' + error.message);
}
// 当发生异常时,系统调用的错误回调接口
catchCallback(error) {
console.info('catch error happened, error name is ' + error.name);
console.info('catch error happened, error code is ' + error.code);
console.info('catch error happened, error message is ' + error.message);
}
async videoRecorderDemo() {
let videoRecorder = null; // videoRecorder空对象在createVideoRecorder成功后赋值
let surfaceID = null; // 用于保存getInputSurface返回的surfaceID
// 获取需要录制的视频的fd地址
await this.getFd('01.mp4');
// 录制相关参数配置,配置参数以实际硬件设备支持的范围为准
let videoProfile = {
audioBitrate : 48000,
audioChannels : 2,
audioCodec : 'audio/mp4a-latm',
audioSampleRate : 48000,
fileFormat : 'mp4',
videoBitrate : 2000000,
videoCodec : 'video/avc',
videoFrameWidth : 640,
videoFrameHeight : 480,
videoFrameRate : 30
}
let videoConfig = {
audioSourceType : 1,
videoSourceType : 0,
profile : videoProfile,
url : this.testFdNumber, // testFdNumber由getFd生成
orientationHint : 0,
location : { latitude : 30, longitude : 130 }
}
// 创建videoRecorder对象
await media.createVideoRecorder().then((recorder) => {
console.info('case createVideoRecorder called');
if (typeof (recorder) != 'undefined') {
videoRecorder = recorder;
console.info('createVideoRecorder success');
} else {
console.info('createVideoRecorder failed');
}
}, this.failureCallback).catch(this.catchCallback);
// 调用prepare完成视频录制前的准备工作
await videoRecorder.prepare(videoConfig).then(() => {
console.info('prepare success');
}, this.failureCallback).catch(this.catchCallback);
// 获取surfaceID并保存下来传递给camera相关接口
await videoRecorder.getInputSurface().then((surface) => {
console.info('getInputSurface success');
surfaceID = surface;
}, this.failureCallback).catch(this.catchCallback);
// 视频录制依赖相机相关接口,以下需要先调用相机起流接口后才能继续执行,具体的相机接口调用请参考sample用例
// 视频录制启动接口
await videoRecorder.start().then(() => {
console.info('start success');
}, this.failureCallback).catch(this.catchCallback);
// 调用pause接口时需要暂停camera出流
await videoRecorder.pause().then(() => {
console.info('pause success');
}, this.failureCallback).catch(this.catchCallback);
// 调用resume接口时需要恢复camera出流
await videoRecorder.resume().then(() => {
console.info('resume success');
}, this.failureCallback).catch(this.catchCallback);
// 停止camera出流后,停止视频录制
await videoRecorder.stop().then(() => {
console.info('stop success');
}, this.failureCallback).catch(this.catchCallback);
// 重置录制相关配置
await videoRecorder.reset().then(() => {
console.info('reset success');
}, this.failureCallback).catch(this.catchCallback);
// 释放视频录制相关资源并释放camera对象相关资源
await videoRecorder.release().then(() => {
console.info('release success');
}, this.failureCallback).catch(this.catchCallback);
// 相关对象置null
videoRecorder = undefined;
surfaceID = undefined;
}
}
```
\ No newline at end of file
# 视频录制
在OpenHarmony系统中,当前仅支持AVRecorder开发视频录制,集成了音频捕获,音频编码,视频编码,音视频封装功能,适用于实现简单视频录制并直接得到视频本地文件的场景。
本开发指导将以“开始录制-暂停录制-恢复录制-停止录制”的一次流程为示例,向开发者讲解如何使用AVRecorder进行视频录制。
在进行应用开发的过程中,开发者可以通过AVRecorder的state属性主动获取当前状态,或使用on('stateChange')方法监听状态变化。开发过程中应该严格遵循状态机要求,例如只能在started状态下调用pause()接口,只能在paused状态下调用resume()接口。
**图1** 录制状态变化示意图  
![Recording status change](figures/video-recording-status-change.png)
状态的详细说明请参考[AVRecorderState](../reference/apis/js-apis-media.md#avrecorderstate9)
## 开发步骤及注意事项
> **说明:**
>
> AVRecorder只负责视频数据的处理,需要与视频数据采集模块配合才能完成视频录制。视频数据采集模块需要通过Surface将视频数据传递给AVRecorder进行数据处理。当前常用的数据采集模块为相机模块,相机模块目前仅对系统应用开放,具体请参考[相机模块](camera.md)。
AVRecorder详细的API说明请参考[AVRecorder API参考](../reference/apis/js-apis-media.md#avrecorder9)
1. 创建AVRecorder实例,实例创建完成进入idle状态。
```ts
import media from '@ohos.multimedia.media'
let avRecorder
media.createAVRecorder().then((recorder) => {
avRecorder = recorder
}, (error) => {
console.error('createAVRecorder failed')
})
```
2. 设置业务需要的监听事件,监听状态变化及错误上报。
| 事件类型 | 说明 |
| -------- | -------- |
| stateChange | 必要事件,监听播放器的state属性改变 |
| error | 必要事件,监听播放器的错误信息 |
```ts
// 状态上报回调函数
avRecorder.on('stateChange', (state, reason) => {
console.info('current state is: ' + state);
})
// 错误上报回调函数
avRecorder.on('error', (err) => {
console.error('error happened, error message is ' + err);
})
```
3. 配置视频录制参数,调用prepare()接口,此时进入prepared状态。
> **说明:**
>
> 配置参数需要注意:
>
> - prepare接口的入参avConfig中仅设置视频相关的配置参数,如示例代码所示。
> 如果添加了音频参数,系统将认为是“音频+视频录制”。
>
> - 需要使用支持的[录制规格](avplayer-avrecorder-overview.md#支持的格式),视频比特率、分辨率、帧率以实际硬件设备支持的范围为准。
>
> - 录制输出的url地址(即示例里avConfig中的url),形式为fd://xx (fd number)。需要调用基础文件操作接口([ohos.file.fs](../reference/apis/js-apis-file-fs.md))实现应用文件访问能力,获取方式参考[应用文件访问与管理](../file-management/app-file-access.md)。
```ts
let avProfile = {
fileFormat : media.ContainerFormatType.CFT_MPEG_4, // 视频文件封装格式,只支持MP4
videoBitrate : 200000, // 视频比特率
videoCodec : media.CodecMimeType.VIDEO_AVC, // 视频文件编码格式,支持mpeg4和avc两种格式
videoFrameWidth : 640, // 视频分辨率的宽
videoFrameHeight : 480, // 视频分辨率的高
videoFrameRate : 30 // 视频帧率
}
let avConfig = {
videoSourceType : media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, // 视频源类型,支持YUV和ES两种格式
profile : this.avProfile,
url : 'fd://35', // 参考应用文件访问与管理开发示例新建并读写一个文件
rotation : 0, // 视频旋转角度,默认为0不旋转,支持的值为0、90、180、270
}
avRecorder.prepare(avConfig).then(() => {
console.info('avRecorder prepare success')
}, (error) => {
console.error('avRecorder prepare failed')
})
```
4. 获取视频录制需要的SurfaceID。
调用getInputSurface()接口,接口的返回值SurfaceID用于传递给视频数据输入源模块。常用的输入源模块为相机,以下示例代码中,采用相机作为视频输入源为例。
输入源模块通过SurfaceID可以获取到Surface,通过Surface可以将视频数据流传递给AVRecorder,由AVRecorder再进行视频数据的处理。
```ts
avRecorder.getInputSurface().then((surfaceId) => {
console.info('avRecorder getInputSurface success')
}, (error) => {
console.error('avRecorder getInputSurface failed')
})
```
5. 初始化视频数据输入源。该步骤需要在输入源模块完成,以相机为例,需要创建录像输出流,包括创建Camera对象、获取相机列表、创建相机输入流等,相机详细步骤请参考[相机-录像实现方案](camera-recording-case.md)
6. 开始录制,启动输入源输入视频数据,例如相机模块调用camera.VideoOutput.start接口启动相机录制。然后调用AVRecorder.start()接口,此时AVRecorder进入started状态。
7. 暂停录制,调用pause()接口,此时AVRecorder进入paused状态,同时暂停输入源输入数据。例如相机模块调用camera.VideoOutput.stop停止相机视频数据输入。
8. 恢复录制,调用resume()接口,此时再次进入started状态。
9. 停止录制,调用stop()接口,此时进入stopped状态,同时停止相机录制。
10. 重置资源,调用reset()重新进入idle状态,允许重新配置录制参数。
11. 销毁实例,调用release()进入released状态,退出录制,释放视频数据输入源相关资源,例如相机资源。
## 完整示例
参考以下示例,完成“开始录制-暂停录制-恢复录制-停止录制”的完整流程。
```ts
import media from '@ohos.multimedia.media'
const TAG = 'VideoRecorderDemo:'
export class VideoRecorderDemo {
private avRecorder;
private videoOutSurfaceId;
private avProfile = {
fileFormat : media.ContainerFormatType.CFT_MPEG_4, // 视频文件封装格式,只支持MP4
videoBitrate : 100000, // 视频比特率
videoCodec : media.CodecMimeType.VIDEO_AVC, // 视频文件编码格式,支持mpeg4和avc两种格式
videoFrameWidth : 640, // 视频分辨率的宽
videoFrameHeight : 480, // 视频分辨率的高
videoFrameRate : 30 // 视频帧率
}
private avConfig = {
videoSourceType : media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, // 视频源类型,支持YUV和ES两种格式
profile : this.avProfile,
url : 'fd://35', // 参考应用文件访问与管理开发示例新建并读写一个文件
rotation : 0, // 视频旋转角度,默认为0不旋转,支持的值为0、90、180、270
}
// 注册avRecorder回调函数
setAvRecorderCallback() {
// 状态机变化回调函数
this.avRecorder.on('stateChange', (state, reason) => {
console.info(TAG + 'current state is: ' + state);
})
// 错误上报回调函数
this.avRecorder.on('error', (err) => {
console.error(TAG + 'error ocConstantSourceNode, error message is ' + err);
})
}
// 相机相关准备工作
async prepareCamera() {
// 具体实现查看相机资料
}
// 启动相机出流
async startCameraOutput() {
// 调用VideoOutput的start接口开始录像输出
}
// 停止相机出流
async stopCameraOutput() {
// 调用VideoOutput的stop接口停止录像输出
}
// 释放相机实例
async releaseCamera() {
// 释放相机准备阶段创建出的实例
}
// 开始录制对应的流程
async startRecordingProcess() {
// 1.创建录制实例;
this.avRecorder = await media.createAVRecorder();
this.setAvRecorderCallback();
// 2. 获取录制文件fd;获取到的值传递给avConfig里的url,实现略
// 3.配置录制参数完成准备工作
await this.avRecorder.prepare(this.avConfig);
this.videoOutSurfaceId = await this.avRecorder.getInputSurface();
// 4.完成相机相关准备工作
await this.prepareCamera();
// 5.启动相机出流
await this.startCameraOutput();
// 6. 启动录制
await this.videoRecorder.start();
}
// 暂停录制对应的流程
async pauseRecordingProcess() {
if (this.avRecorder.state === 'started') { // 仅在started状态下调用pause为合理状态切换
await this.avRecorder.pause();
await this.stopCameraOutput(); // 停止相机出流
}
}
// 恢复录制对应的流程
async resumeRecordingProcess() {
if (this.avRecorder.state === 'paused') { // 仅在paused状态下调用resume为合理状态切换
await this.startCameraOutput(); // 启动相机出流
await this.avRecorder.resume();
}
}
async stopRecordingProcess() {
// 1. 停止录制
if (this.avRecorder.state === 'started'
|| this.avRecorder.state === 'paused' ) { // 仅在started或者paused状态下调用stop为合理状态切换
await this.avRecorder.stop();
await this.stopCameraOutput();
}
// 2.重置
await this.avRecorder.reset();
// 3.释放录制实例
await this.avRecorder.release();
// 4.文件录制完成后,关闭fd,实现略
// 5.释放相机相关实例
await this.releaseCamera();
}
// 一个完整的【开始录制-暂停录制-恢复录制-停止录制】示例
async videoRecorderDemo() {
await this.startRecordingProcess(); // 开始录制
// 用户此处可以自行设置录制时长,例如通过设置休眠阻止代码执行
await this.pauseRecordingProcess(); //暂停录制
await this.resumeRecordingProcess(); // 恢复录制
await this.stopRecordingProcess(); // 停止录制
}
}
```
# 播放音量管理
播放音量的管理主要包括对系统音量的管理和对音频流音量的管理。系统音量与音频流音量分别是指OpenHarmony系统的总音量和指定音频流的音量,其中音频流音量的大小受制于系统音量,管理两者的接口不同。
## 系统音量
管理系统音量的接口是AudioVolumeManager,在使用之前,需要使用getVolumeManager()获取AudioVolumeManager实例。目前该接口只能获取音量信息及监听音量变化,不能主动调节系统音量。
```ts
import audio from '@ohos.multimedia.audio';
let audioManager = audio.getAudioManager();
let audioVolumeManager = audioManager.getVolumeManager();
```
### 监听系统音量变化
通过设置监听事件,可以监听系统音量的变化:
```ts
audioVolumeManager.on('volumeChange', (volumeEvent) => {
console.info(`VolumeType of stream: ${volumeEvent.volumeType} `);
console.info(`Volume level: ${volumeEvent.volume} `);
console.info(`Whether to updateUI: ${volumeEvent.updateUi} `);
});
```
### 调节系统音量(仅对系统应用开放)
目前调节系统音量主要是靠SystemAPI,具体服务于物理音量按键和设置。通过音量按键可以调节系统音量的大小,根据按下的具体按键调用系统接口,实现系统音量的大小调节。调节的音量类型包括媒体、铃声和通知。
## 音频流音量
管理音频流音量的接口是AVPlayer或AudioRenderer的setVolume()方法,使用AVPlayer设置音频流音量的示例代码如下:
```ts
let volume = 1.0 // 指定的音量大小,取值范围为[0.00-1.00],1表示最大音量
avPlayer.setVolume(volume)
```
使用AudioRenderer设置音频流音量的示例代码如下:
```ts
audioRenderer.setVolume(0.5).then(data=>{ // 音量范围为[0.0-1.0]
console.info('Invoke setVolume succeeded.');
}).catch((err) => {
console.error(`Invoke setVolume failed, code is ${err.code}, message is ${err.message}`);
});
```
......@@ -350,7 +350,7 @@ Codec MIME类型枚举。
播放管理类,用于管理和播放媒体资源。在调用AVPlayer的方法前,需要先通过[createAVPlayer()](#mediacreateavplayer9)构建一个AVPlayer实例。
Audio/Video播放demo可参考:[AVPlayer开发指导](../../media/avplayer-playback.md)
Audio/Video播放demo可参考:[音频播放开发指导](../../media/using-avplayer-for-playback.md)[视频播放开发指导](../../media/video-playback.md)
### 属性<a name=avplayer_属性></a>
......@@ -1602,7 +1602,7 @@ avPlayer.off('audioInterrupt')
## AVPlayerState<sup>9+</sup><a name = avplayerstate></a>
[AVPlayer](#avplayer9)的状态机,可通过state属性主动获取当前状态,也可通过监听[stateChange](#stateChange_on)事件上报当前状态,状态机之间的切换规则,可参考[AVPlayer播放器开发指导](../../media/avplayer-playback.md)
[AVPlayer](#avplayer9)的状态机,可通过state属性主动获取当前状态,也可通过监听[stateChange](#stateChange_on)事件上报当前状态,状态机之间的切换规则,可参考[音频播放开发指导](../../media/using-avplayer-for-playback.md)
**系统能力:** SystemCapability.Multimedia.Media.AVPlayer
......@@ -1709,7 +1709,11 @@ audioPlayer.getTrackDescription((error, arrList) => {
音视频录制管理类,用于音视频媒体录制。在调用AVRecorder的方法前,需要先通过createAVRecorder()构建一个AVRecorder实例。
音视频录制demo可参考:[音视频录制开发指导](../../media/avrecorder.md)
音视频录制demo可参考:[音频录制开发指导](../../media/using-avrecorder-for-recording.md)[视频录制开发指导](../../media/video-recording.md)
> **说明:**
>
> 使用相机进行视频录制时,需要与相机模块配合,相机模块接口开放状态以及使用详情见[相机管理](js-apis-camera.md)。
### 属性
......@@ -2591,8 +2595,6 @@ avRecorder.off('error');
视频录制管理类,用于录制视频媒体。在调用VideoRecorder的方法前,需要先通过[createVideoRecorder()](#mediacreatevideorecorder9)构建一个[VideoRecorder](#videorecorder9)实例。
视频录制demo可参考:[视频录制开发指导](../../media/video-recorder.md)
### 属性
**系统能力:** SystemCapability.Multimedia.Media.VideoRecorder
......@@ -3918,8 +3920,6 @@ audioPlayer.setVolume(3); //设置volume为无效值,触发'error'事件
视频播放管理类,用于管理和播放视频媒体。在调用VideoPlayer的方法前,需要先通过[createVideoPlayer()](#createvideoplayer)构建一个VideoPlayer实例。
视频播放demo可参考:[视频播放开发指导](../../media/video-playback.md)
### 属性<a name=videoplayer_属性></a>
**系统能力:** SystemCapability.Multimedia.Media.VideoPlayer
......@@ -4738,8 +4738,6 @@ videoPlayer.url = 'fd://error'; //设置错误的播放地址,触发'error'
音频录制管理类,用于录制音频媒体。在调用AudioRecorder的方法前,需要先通过[createAudioRecorder()](#mediacreateaudiorecorder) 构建一个AudioRecorder实例。
音频录制demo可参考:[音频录制开发指导](../../media/audio-recorder.md)
### prepare<a name=audiorecorder_prepare></a>
prepare(config: AudioRecorderConfig): void
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册