本示例通过CameraKit自定义相机,并通过AVRecorder进行录像。帮助开发者在自定义相机的场景开发中,实现使用AVRecorder录像的功能。
一、案例实战
(一) 案例效果截图
| 获取权限 | 录制页 | 录制中 | 主页 |
|---|---|---|---|
(二) 案例运用到的知识点
- 核心知识点
- 通过cameraInput获取相机采集数据,创建相机输入。
- 创建previewOutput,获取预览输出流,通过XComponent的surfaceId连接,送显XComponent。
- 通过AVRecorder的surfaceId创建录像输出流VideoOutput输出到文件中。
- 其他知识点
- ArkTS 语言基础
- V2版状态管理:@ComponentV2/@Local/@Param
- 自定义组件和组件生命周期
- 内置组件:Stack/Scroll/Flex/Column/Row/Text/Image/Button
- 提示框:promptAction
- 图形绘制:Canvas
- UI范式渲染控制:ForEach/if
- 日志管理类的编写
- 常量与资源分类的访问
- MVVM模式
(三) 相关权限
- 允许应用使用相机:ohos.permission.CAMERA。
- 允许应用使用麦克风:ohos.permission.MICROPHONE。
- 允许应用访问用户媒体文件中的地理位置信息:ohos.permission.MEDIA_LOCATION。
(四) 代码结构
├──entry/src/main/ets/
│ ├── common
│ │ └── CommonConstants.ets // 公共常量类
│ ├── entryability
│ │ └── EntryAbility.ets // 程序入口类
│ ├── pages
│ │ ├── Index.ets // 首页
│ │ └── Record.ets // 视频录制页
│ └── utils
│ ├── FileUtil.ets // 文件工具类
│ ├── Logger.ets // 日志工具类
│ └── RouterParams.ets // 路由参数类
└──entry/src/main/resources // 应用静态资源目录
(五) 公共文件与资源
本案例涉及到共常量类和日志类代码如下。
- 公共常量类(CommonConstants)
// entry/src/main/ets/common/CommonConstants.ets
export class CommonConstants {
/**
* 显示 Toast 的持续时间,单位为毫秒。
* 例如用于 promptAction.showToast 中的 duration 设置。
*/
static readonly SHOW_TOAST_DURATION: number = 4000
/**
* Toast 弹出位置距离底部的距离,单位为像素。
* 一般用于微调弹窗在屏幕上的位置。
*/
static readonly SHOW_TOAST_BOTTOM: number = 108
/**
* 图片尺寸,单位为像素。
* 可用于统一缩略图、头像等控件的尺寸。
*/
static readonly IMAGE_SIZE: number = 200
/**
* 宽度/高度占组件的 100%,即全屏/全宽。
*/
static readonly FULL_PERCENT: string = '100%'
/**
* 组件宽度或高度占比为 90%。
*/
static readonly NINETY_PERCENT: string = '90%'
/**
* 组件宽度或高度占比为 70%。
*/
static readonly SEVENTY_PERCENT: string = '70%'
/**
* 页面底部间距占比为 15%。
* 通常用于为底部按钮或区域预留空间。
*/
static readonly FIFTEEN_PERCENT: string = '15%'
}
2. 日志类(Logger)
// entry/src/main/ets/utils/Logger.ets
import { hilog } from '@kit.PerformanceAnalysisKit'
class Logger {
private domain: number
private prefix: string
private format: string = '%{public}s, %{public}s'
constructor(prefix: string) {
this.prefix = prefix
this.domain = 0xFF00
}
debug(...args: string[]): void {
hilog.debug(this.domain, this.prefix, this.format, args)
}
info(...args: string[]): void {
hilog.info(this.domain, this.prefix, this.format, args)
}
warn(...args: string[]): void {
hilog.warn(this.domain, this.prefix, this.format, args)
}
error(...args: string[]): void {
hilog.error(this.domain, this.prefix, this.format, args)
}
}
export default new Logger('[CameraKitAVRecorder]')
本案例涉及到的资源文件如下:
- string.json
// main/resources/base/element/string.json
{
"string": [
{
"name": "module_desc",
"value": "模块描述"
},
{
"name": "EntryAbility_desc",
"value": "description"
},
{
"name": "EntryAbility_label",
"value": "CameraKitAVRecorder"
},
{
"name": "reason",
"value": "用于录制视频"
},
{
"name": "reason2",
"value": "用于读取录制视频"
},
{
"name": "button_text",
"value": "录制视频"
},
{
"name": "show_toast_message",
"value": "有未开启的权限,请去\“设置->应用与元服务->CameraKitAVRecorder\”中打开所有限制权限"
}
]
}
2. float.json
// entry/src/main/resources/base/element/float.json
{
"float": [
{
"name": "bottom_font_size",
"value": "16fp"
},
{
"name": "bottom_height",
"value": "45vp"
},
{
"name": "bottom_margin_bottom",
"value": "44vp"
},
{
"name": "video_height",
"value": "500vp"
},
{
"name": "video_margin_bottom",
"value": "100vp"
},
{
"name": "image_height",
"value": "120vp"
},
{
"name": "image_margin_bottom",
"value": "60vp"
}
]
}
3. color.json
// entry/src/main/resources/base/element/color.json
{
"color": [
{
"name": "start_window_background",
"value": "#FFFFFF"
},
{
"name": "bottom_background",
"value": "#0080FF"
}
]
}
其他资源请到源码中获取。
(六) 首页
实现了请求相机、麦克风等权限,校验权限,展示视频文件,点击按钮跳转到录制页面(Record 页面)。
- 首页面
// entry/src/main/ets/pages/Index.ets
// 引入所需模块
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { promptAction, router } from '@kit.ArkUI'
import { rpc } from '@kit.IPCKit'
import Logger from '../utils/Logger'
import { CommonConstants } from '../common/CommonConstants'
import { RouterParams } from '../utils/RouterParams'
const TAG: string = 'Index' // 日志标识
const context = getContext() as common.UIAbilityContext // 获取当前上下文对象
@Entry
@ComponentV2
struct Index {
@Local path: string = '' // 视频路径
private controller: VideoController | undefined // 视频控制器
private result: boolean = false // 权限校验结果
atManager: abilityAccessCtrl.AtManager
= abilityAccessCtrl.createAtManager() // 权限管理器实例
// 所需权限列表:相机、麦克风、媒体位置
permissions: Array<Permissions> = [
'ohos.permission.CAMERA',
'ohos.permission.MICROPHONE',
'ohos.permission.MEDIA_LOCATION'
]
// 页面显示时从路由参数中获取传入的视频路径
onPageShow() {
try {
this.path = (router.getParams() as RouterParams).data
} catch (e) {
return // 捕获异常防止程序崩溃
}
}
// 页面首次加载时请求权限
async aboutToAppear() {
this.reqPermissionsFromUser(this.permissions, context)
}
// 请求用户授予权限
reqPermissionsFromUser(
permissions: Array<Permissions>,
context: common.UIAbilityContext
): void {
this.atManager.requestPermissionsFromUser(context, permissions)
.then((data) => {
let grantStatus: Array<number> = data.authResults // 获取每项权限结果
let length: number = grantStatus.length
for (let i = 0; i < length; i++) {
if (grantStatus[i] === 0) {
Logger.info(TAG, 'User authorized.') // 权限授权成功
} else {
Logger.info(TAG, 'User denied authorization.') // 有权限被拒绝
return
}
}
}).catch((err: BusinessError) => {
// 捕获异常并打印日志
Logger.error(`Failed to request permissions from user. `
+ `Code is ${err.code}, message is ${err.message}`)
})
}
// 校验当前权限是否已授权
checkAccessToken(permissions: Array<Permissions>) {
let callerTokenId: number
= rpc.IPCSkeleton.getCallingTokenId() // 获取当前调用方 TokenId
let atManager: abilityAccessCtrl.AtManager
= abilityAccessCtrl.createAtManager()
try {
for (let i = 0; i < permissions.length; i++) {
let data: abilityAccessCtrl.GrantStatus
= atManager.verifyAccessTokenSync(callerTokenId, permissions[i])
if (data === -1) {
this.result = false // 未授权
} else {
this.result = true // 已授权
}
// 如果有权限未授权,显示提示并中止
if (!this.result) {
promptAction.showToast({
message: $r('app.string.show_toast_message'), // 提示语句资源
duration: CommonConstants.SHOW_TOAST_DURATION,
bottom: CommonConstants.SHOW_TOAST_BOTTOM
})
break
}
}
} catch (err) {
Logger.error(
TAG,
`checkAccessToken catch err->${JSON.stringify(err)}`
) // 打印异常日志
}
}
build() {
Stack({ alignContent: Alignment.BottomStart }) {
Column() {
// 如果路径有效,则显示视频组件
if (this.path) {
Video({
src: 'file://' + this.path, // 加载本地视频路径
controller: this.controller // 绑定视频控制器
})
.height($r('app.float.video_height'))
.margin({ bottom: $r('app.float.video_margin_bottom') })
}
// 按钮:点击后校验权限并跳转至 Record 页面
Button($r('app.string.button_text'))
.width(CommonConstants.NINETY_PERCENT)
.height($r('app.float.bottom_height'))
.backgroundColor($r('app.color.bottom_background'))
.fontSize($r('app.float.bottom_font_size'))
.fontColor(Color.White)
.margin({ bottom: $r('app.float.bottom_margin_bottom') })
.onClick(async () => {
this.checkAccessToken(this.permissions)
if (this.result) {
router.pushUrl({ url: 'pages/Record' }) // 跳转录制页面
}
})
}
.width(CommonConstants.FULL_PERCENT)
}
.width(CommonConstants.FULL_PERCENT)
.height(CommonConstants.FULL_PERCENT)
.backgroundColor(Color.White)
}
}
2. 路由参数类
// entry/src/main/ets/utils/RouterParams.ets
export class RouterParams {
text: string
data: string
constructor(str: string, data: string) {
this.text = str
this.data = data
}
}
3. 权限设置
// entry/src/main/module.json5
{
"module": {
...
"requestPermissions": [
{
"name": "ohos.permission.CAMERA",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when": "always"
}
},
{
"name": "ohos.permission.MEDIA_LOCATION",
"reason": "$string:reason2",
"usedScene": {
"abilities": [
"FormAbility"
],
"when": "always"
}
}
]
}
}
(七) 视频录制页
- 录制页面
// entry/src/main/ets/pages/Record.ets
// 引入相关模块和工具类
import { common } from '@kit.AbilityKit'
import { camera } from '@kit.CameraKit'
import { media } from '@kit.MediaKit'
import { fileUri } from '@kit.CoreFileKit'
import { router } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'
import Logger from '../utils/Logger'
import { FileUtil } from '../utils/FileUtil'
import { CommonConstants } from '../common/CommonConstants'
import { RouterParams } from '../utils/RouterParams'
// 日志标签
const TAG: string = 'Record'
// 获取当前UIAbility的上下文
let context = getContext(this) as common.UIAbilityContext
@Entry
@Component
struct Record {
// 定义状态变量(用于UI响应式更新)
@State xComponentWidth: number = 0
@State xComponentHeight: number = 0
@State videoUri: string = '' // 视频文件的Uri
@State recording: boolean = false // 是否正在录制
@State path: string = '' // 视频保存路径
@State cameraManager: camera.CameraManager | undefined = undefined
@State videoOutput: camera.VideoOutput | undefined = undefined
@State captureSession: camera.Session | undefined = undefined
@State cameraInput: camera.CameraInput | undefined = undefined
@State previewOutput: camera.PreviewOutput | undefined = undefined
@State avRecorder: media.AVRecorder | undefined = undefined
private mXComponentController: XComponentController
= new XComponentController
private surfaceId: string = '' // XComponent 渲染表面 ID
url: string = '' // 视频保存的 fd 形式 URL
// 页面加载时调用
aboutToAppear() {
// 构建保存路径
this.path = context.filesDir + '/' + 'VIDEO_'
+ Date.parse(new Date().toString()) + '.mp4'
let file = FileUtil.createOrOpen(this.path)
this.url = 'fd://' + file.fd
this.videoUri = fileUri.getUriFromPath(this.path)
}
// 构建页面视图
build() {
Stack({ alignContent: Alignment.Top }) {
// 相机预览区域
XComponent({
id: 'componentId',
type: XComponentType.SURFACE,
controller: this.mXComponentController,
})
.onLoad(async () => {
// 获取 Surface ID 并初始化相机
this.surfaceId = this.mXComponentController.getXComponentSurfaceId()
this.mXComponentController.setXComponentSurfaceRect({
surfaceWidth: 1080,
surfaceHeight: 1920
})
await this.initCamera(getContext(this), this.surfaceId)
})
.height(CommonConstants.SEVENTY_PERCENT)
.margin({
top: CommonConstants.FIFTEEN_PERCENT
})
// 控制按钮区域
Column() {
Blank()
Row() {
// 开始/停止录制按钮
Image(this.recording
? $r('app.media.camera_pause_video_4x')
: $r('app.media.camera_take_video_4x'))
.width(px2vp(CommonConstants.IMAGE_SIZE))
.height(px2vp(CommonConstants.IMAGE_SIZE))
.onClick(async () => {
if (this.recording) {
// 停止录制,回传文件路径
await this.stopRecord()
let options: router.RouterOptions = {
url: '',
params: new RouterParams('Sandbox path', this.path)
}
router.back(options)
} else {
// 开始录制
this.startRecord()
}
this.recording = !this.recording
})
}
.width(CommonConstants.FULL_PERCENT)
.height($r('app.float.image_height'))
.margin({ bottom: $r('app.float.image_margin_bottom') })
.justifyContent(FlexAlign.Center)
.alignItems(VerticalAlign.Center)
}
.width(CommonConstants.FULL_PERCENT)
.height(CommonConstants.FULL_PERCENT)
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Start)
}
.backgroundColor(Color.Black)
.width(CommonConstants.FULL_PERCENT)
.height(CommonConstants.FULL_PERCENT)
}
// 初始化相机和录制配置
async initCamera(context: common.Context, surfaceId: string) {
// 获取相机管理器
this.cameraManager = camera.getCameraManager(context)
if (!this.cameraManager) {
Logger.error(TAG, 'camera.getCameraManager error')
return
}
// 注册相机状态监听器
this.cameraManager.on('cameraStatus', (
err: BusinessError,
cameraStatusInfo: camera.CameraStatusInfo
) => {
Logger.info(TAG, `camera : ${cameraStatusInfo.camera.cameraId}`)
Logger.info(TAG, `status: ${cameraStatusInfo.status}`)
})
// 获取支持的相机设备
let cameraArray: Array<camera.CameraDevice> = []
try {
cameraArray = this.cameraManager.getSupportedCameras()
} catch (error) {
let err = error as BusinessError
Logger.error(TAG,
`getSupportedCameras call failed. error code: ${err.code}`)
}
if (cameraArray.length <= 0) {
Logger.error(TAG, 'cameraManager.getSupportedCameras error')
return
}
// 获取相机输出能力
let cameraOutputCap = this.cameraManager.getSupportedOutputCapability(
cameraArray[0], camera.SceneMode.NORMAL_VIDEO)
if (!cameraOutputCap) {
Logger.error(TAG, 'cameraManager.getSupportedOutputCapability error')
return
}
// 查找视频输出支持的分辨率
let videoSize: camera.Size = { width: 1920, height: 1080 }
let videoProfile = cameraOutputCap.videoProfiles.find((profile) =>
profile.size.width === videoSize.width
&& profile.size.height === videoSize.height)
if (!videoProfile) {
Logger.error(TAG, 'videoProfile is not found')
return
}
// 构建 AVRecorder 配置
let aVRecorderProfile: media.AVRecorderProfile = {
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: videoSize.width,
videoFrameHeight: videoSize.height,
videoFrameRate: 30
}
let aVRecorderConfig: media.AVRecorderConfig = {
audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_MIC,
videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
profile: aVRecorderProfile,
url: this.url,
rotation: 90,
location: { latitude: 30, longitude: 130 }
}
// 创建 AVRecorder 实例
try {
this.avRecorder = await media.createAVRecorder()
await this.avRecorder.prepare(aVRecorderConfig)
} catch (error) {
Logger.error(TAG, `AVRecorder 初始化失败: ${JSON.stringify(error)}`)
}
if (!this.avRecorder) return
// 获取视频输入的 surfaceId
let videoSurfaceId: string | undefined = undefined
try {
videoSurfaceId = await this.avRecorder.getInputSurface()
} catch (error) {
Logger.error(TAG, `getInputSurface 失败: ${JSON.stringify(error)}`)
}
if (!videoSurfaceId) return
// 创建 VideoOutput 对象
try {
this.videoOutput
= this.cameraManager.createVideoOutput(videoProfile, videoSurfaceId)
} catch (error) {
Logger.error(TAG, `createVideoOutput 失败: ${JSON.stringify(error)}`)
}
if (!this.videoOutput) return
// 创建并配置会话
try {
this.captureSession
= this.cameraManager.createSession(camera.SceneMode.NORMAL_VIDEO)
this.captureSession.beginConfig()
this.cameraInput = this.cameraManager.createCameraInput(cameraArray[0])
await this.cameraInput.open()
this.captureSession.addInput(this.cameraInput)
this.previewOutput
= this.cameraManager.createPreviewOutput(videoProfile, surfaceId)
this.captureSession.addOutput(this.previewOutput)
this.captureSession.addOutput(this.videoOutput)
await this.captureSession.commitConfig()
await this.captureSession.start()
} catch (error) {
Logger.error(TAG, `captureSession 配置失败: ${JSON.stringify(error)}`)
}
// 启动视频输出
this.videoOutput.start((err: BusinessError) => {
if (err) {
Logger.error(TAG, `VideoOutput 启动失败: ${JSON.stringify(err)}`)
return
}
Logger.info(TAG, '视频输出成功启动')
})
}
// 开始录制
async startRecord() {
if (this.avRecorder) {
try {
await this.avRecorder.start()
} catch (error) {
Logger.error(TAG, `avRecorder start error: ${JSON.stringify(error)}`)
}
}
}
// 停止录制
async stopRecord() {
if (this.avRecorder) {
try {
if (this.videoOutput) {
this.videoOutput.stop((err: BusinessError) => {
if (err) {
Logger.error(TAG,
`videoOutput stop error: ${JSON.stringify(err)}`)
return
}
Logger.info(TAG, '视频输出成功停止')
})
}
await this.avRecorder.stop()
await this.avRecorder.release()
// 释放资源
this.captureSession?.stop()
this.cameraInput?.close()
this.previewOutput?.release()
this.videoOutput?.release()
this.captureSession?.release()
this.captureSession = undefined
} catch (error) {
Logger.error(TAG, `停止录制出错: ${JSON.stringify(error)}`)
}
}
}
}
2. 文件管理工具类
import { fileIo } from '@kit.CoreFileKit'
import Logger from './Logger'
/**
* 文件操作工具类,封装文件的创建、写入等常用操作。
*/
export class FileUtil {
/**
* 创建或打开一个文件。
* 如果文件已存在,则以读写模式打开;否则新建文件并以读写模式打开。
* @param path 文件路径
* @returns 打开的文件对象
*/
static createOrOpen(path: string): fileIo.File {
// 检查文件是否存在
let isExist = fileIo.accessSync(path)
let file: fileIo.File
if (isExist) {
// 文件存在,使用读写模式打开
file = fileIo.openSync(path, fileIo.OpenMode.READ_WRITE)
} else {
// 文件不存在,创建文件并使用读写模式打开
file = fileIo.openSync(
path,
fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE
)
}
return file
}
/**
* 将 ArrayBuffer 类型的二进制数据写入指定文件。
* @param path 文件路径
* @param arrayBuffer 要写入的数据
* @returns 写入的字节数;失败返回 -1
*/
static writeBufferToFile(path: string, arrayBuffer: ArrayBuffer): number {
try {
// 以创建+读写模式打开文件
let file = fileIo.openSync(
path,
fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE
)
// 写入数据
let value = fileIo.writeSync(file.fd, arrayBuffer)
// 关闭文件
fileIo.closeSync(file)
return value
} catch (err) {
Logger.error('FileUtil', 'writeFile err:' + err)
return -1
}
}
/**
* 将字符串内容写入文件(覆盖原有内容)。
* @param path 文件路径
* @param text 要写入的字符串
* @returns 写入的字符长度;失败返回 -1
*/
static writeStrToFile(path: string, text: string): number {
try {
// 以创建+读写模式打开文件
let file = fileIo.openSync(
path,
fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE
)
// 写入字符串内容
let value = fileIo.writeSync(file.fd, text)
// 关闭文件
fileIo.closeSync(file)
return value
} catch (err) {
Logger.error('FileUtil', 'writeFile err:' + err)
return -1
}
}
}
二、核心知识点
通过调用Camera Kit(相机服务)提供的接口可以开发相机应用,应用通过访问和操作相机硬件,实现基础操作,如预览、拍照和录像。
(一) 向用户申请授权
在开发相机应用时,需要先申请相机相关权限,确保应用拥有访问相机硬件及其他功能的权限。
- 使用相机拍摄前,需要申请ohos.permission.CAMERA相机权限。
- 当需要使用麦克风同时录制音频时,需要申请ohos.permission.MICROPHONE麦克风权限。
- 当需要拍摄的图片/视频显示地理位置信息时,需要申请ohos.permission.MEDIA_LOCATION,来访问用户媒体文件中的地理位置信息。
1. 约束与限制
- 每次执行需要目标权限的操作时,应用都必须检查自己是否已经具有该权限。如需检查用户是否已向您的应用授予特定权限,可以使用checkAccessToken()函数,此方法会返回PERMISSION_GRANTED或PERMISSION_DENIED。
- 每次访问受目标权限保护的接口之前,都需要使用requestPermissionsFromUser()接口请求相应的权限。用户可能在动态授予权限后通过系统设置来取消应用的权限,因此不能将之前授予的授权状态持久化。
- user_grant权限授权要基于用户可知可控的原则,需要应用在运行时主动调用系统动态申请权限的接口,系统弹框由用户授权,用户结合应用运行场景的上下文,识别出应用申请相应敏感权限的合理性,从而做出正确的选择。
- 系统不鼓励频繁弹窗打扰用户,如果用户拒绝授权,将无法再次拉起弹窗,需要应用引导用户在系统应用“设置”的界面中手动授予权限。
- 系统权限弹窗不可被遮挡。
系统权限弹窗不可被其他组件/控件遮挡,弹窗信息需要完整展示,以便用户识别并完成授权动作。
如果系统权限弹窗与其他组件/控件同时同位置展示,系统权限弹窗将默认覆盖其他组件/控件。
2. 开发步骤
以申请使用位置权限为例进行说明。
效果展示:
- 申请ohos.permission.LOCATION、ohos.permission.APPROXIMATELY_LOCATION权限。
- 校验当前是否已经授权。
在进行权限申请之前,需要先检查当前应用程序是否已经被授予权限。可以通过调用checkAccessToken()方法来校验当前是否已经授权。如果已经授权,则可以直接访问目标操作,否则需要进行下一步操作,即向用户申请授权。
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
async function checkPermissionGrant(
permission: Permissions
): Promise<abilityAccessCtrl.GrantStatus> {
let atManager: abilityAccessCtrl.AtManager
= abilityAccessCtrl.createAtManager()
let grantStatus: abilityAccessCtrl.GrantStatus
= abilityAccessCtrl.GrantStatus.PERMISSION_DENIED
// 获取应用程序的accessTokenID
let tokenId: number = 0
try {
let bundleInfo: bundleManager.BundleInfo
= await bundleManager.getBundleInfoForSelf(
bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION)
let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo
tokenId = appInfo.accessTokenId
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`Failed to get bundle info for self. `
+ `Code is ${err.code}, message is ${err.message}`)
}
// 校验应用是否被授予权限
try {
grantStatus = await atManager.checkAccessToken(tokenId, permission)
} catch (error) {
const err: BusinessError = error as BusinessError;
console.error(`Failed to check access token. `
+ `Code is ${err.code}, message is ${err.message}`)
}
return grantStatus
}
async function checkPermissions(): Promise<void> {
let grantStatus1: boolean = await checkPermissionGrant(
// 获取精确定位权限状态
'ohos.permission.LOCATION')
=== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
let grantStatus2: boolean = await checkPermissionGrant(
'ohos.permission.APPROXIMATELY_LOCATION')
// 获取模糊定位权限状态
=== abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
// 精确定位权限只能跟模糊定位权限一起申请,或者已经有模糊定位权限才能申请精确定位权限
if (grantStatus2 && !grantStatus1) {
// 申请精确定位权限
} else if (!grantStatus1 && !grantStatus2) {
// 申请模糊定位权限与精确定位权限或单独申请模糊定位权限
} else {
// 已经授权,可以继续访问目标操作
}
}
3. 动态向用户申请授权。
动态向用户申请权限是指在应用程序运行时向用户请求授权的过程。可以通过调用requestPermissionsFromUser()方法来实现。该方法接收一个权限列表参数,例如位置、日历、相机、麦克风等。用户可以选择授予权限或者拒绝授权。
可以在UIAbility的onWindowStageCreate()回调中调用requestPermissionsFromUser()方法来动态申请权限,也可以根据业务需要在UI中向用户申请授权。
应用在onWindowStageCreate()回调中申请授权时,需要等待异步接口loadContent()/setUIContent()执行结束后或在loadContent()/setUIContent()回调中调用requestPermissionsFromUser(),否则在Content加载完成前,requestPermissionsFromUser会调用失败。
应用在UIExtensionAbility申请授权时,需要在onWindowStageCreate函数执行结束后或在onWindowStageCreate函数回调中调用requestPermissionsFromUser(),否则在ability加载完成前,requestPermissionsFromUser会调用失败。
- 在UIAbility中向用户申请授权。
// 使用UIExtensionAbility:将import { UIAbility } from '@kit.AbilityKit'
// 替换为import { UIExtensionAbility } from '@kit.AbilityKit'
import {
abilityAccessCtrl, common, Permissions, UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'
const permissions: Array<Permissions>
= ['ohos.permission.LOCATION','ohos.permission.APPROXIMATELY_LOCATION']
// 使用UIExtensionAbility:将common.UIAbilityContext
// 替换为common.UIExtensionContext
function reqPermissionsFromUser(
permissions: Array<Permissions>,
context: common.UIAbilityContext): void {
let atManager: abilityAccessCtrl.AtManager
= abilityAccessCtrl.createAtManager()
// requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
atManager.requestPermissionsFromUser(context, permissions).then((data) => {
let grantStatus: Array<number> = data.authResults
let length: number = grantStatus.length;
for (let i = 0; i < length; i++) {
if (grantStatus[i] === 0) {
// 用户授权,可以继续访问目标操作
} else {
// 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,
// 并引导用户到系统设置中打开相应的权限
return
}
}
// 授权成功
}).catch((err: BusinessError) => {
console.error(`Failed to request permissions from user. `
+ `Code is ${err.code}, message is ${err.message}`)
})
}
// 使用UIExtensionAbility:将 UIAbility 替换为UIExtensionAbility
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// ...
windowStage.loadContent('pages/Index', (err, data) => {
reqPermissionsFromUser(permissions, this.context);
// ...
})
}
// ...
}
- 在UI中向用户申请授权。
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
const permissions: Array<Permissions>
= ['ohos.permission.LOCATION','ohos.permission.APPROXIMATELY_LOCATION']
// 使用UIExtensionAbility:将common.UIAbilityContext
// 替换为common.UIExtensionContext
function reqPermissionsFromUser(
permissions: Array<Permissions>,
context: common.UIAbilityContext): void {
let atManager: abilityAccessCtrl.AtManager
= abilityAccessCtrl.createAtManager();
// requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
atManager.requestPermissionsFromUser(context, permissions).then((data) => {
let grantStatus: Array<number> = data.authResults
let length: number = grantStatus.length
for (let i = 0; i < length; i++) {
if (grantStatus[i] === 0) {
// 用户授权,可以继续访问目标操作
} else {
// 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,
// 并引导用户到系统设置中打开相应的权限
return
}
}
// 授权成功
}).catch((err: BusinessError) => {
console.error(`Failed to request permissions from user. `
+ `Code is ${err.code}, message is ${err.message}`)
})
}
@Entry
@Component
struct Index {
aboutToAppear() {
// 使用UIExtensionAbility:将common.UIAbilityContext
// 替换为common.UIExtensionContext
const context: common.UIAbilityContext
= getContext(this) as common.UIAbilityContext
reqPermissionsFromUser(permissions, context)
}
build() {
// ...
}
}
4. 处理授权结果。
调用requestPermissionsFromUser()方法后,应用程序将等待用户授权的结果。如果用户授权,则可以继续访问目标操作。如果用户拒绝授权,则需要提示用户必须授权才能访问当前页面的功能,并引导用户到系统应用“设置”中打开相应的权限。
路径:设置 > 隐私 > 权限管理 > 应用 > 目标应用
(二) 相机管理
在开发一个相机应用前,需要先通过调用相机接口来创建一个独立的相机设备。
1. 开发步骤
- 导入camera接口,接口中提供了相机相关的属性和方法,导入方法如下。
import { camera } from '@kit.CameraKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { common } from '@kit.AbilityKit'
2. 通过getCameraManager方法,获取cameraManager对象。
function getCameraManager(context: common.BaseContext): camera.CameraManager {
let cameraManager: camera.CameraManager = camera.getCameraManager(context)
return cameraManager
}
3. 通过CameraManager类中的getSupportedCameras方法,获取当前设备支持的相机列表,列表中存储了设备支持的所有相机ID。若列表不为空,则说明列表中的每个ID都支持独立创建相机对象;否则,说明当前设备无可用相机,不可继续后续操作。
function getCameraDevices(
cameraManager: camera.CameraManager): Array<camera.CameraDevice> {
let cameraArray: Array<camera.CameraDevice>
= cameraManager.getSupportedCameras()
if (cameraArray != undefined && cameraArray.length > 0) {
for (let index = 0; index < cameraArray.length; index++) {
// 获取相机ID
console.info('cameraId : ' + cameraArray[index].cameraId)
// 获取相机位置
console.info('cameraPosition : ' + cameraArray[index].cameraPosition)
// 获取相机类型
console.info('cameraType : ' + cameraArray[index].cameraType)
// 获取相机连接类型
console.info('connectionType : ' + cameraArray[index].connectionType)
return cameraArray
} else {
console.error("cameraManager.getSupportedCameras error")
return []
}
}
2. 状态监听
在相机应用开发过程中,可以随时监听相机状态,包括新相机的出现、相机的移除、相机的可用状态。在回调函数中,通过相机ID、相机状态这两个参数进行监听,如当有新相机出现时,可以将新相机加入到应用的备用相机中。
通过注册cameraStatus事件,通过回调返回监听结果,callback返回CameraStatusInfo参数,参数的具体内容可参考相机管理器回调接口实例CameraStatusInfo。
function onCameraStatusChange(cameraManager: camera.CameraManager): void {
cameraManager.on(
'cameraStatus',
(err: BusinessError, cameraStatusInfo: camera.CameraStatusInfo) => {
if (err !== undefined && err.code !== 0) {
console.error(`Callback Error, errorCode: ${err.code}`)
return;
}
// 如果当通过USB连接相机设备时,回调函数会返回新的相机出现状态
if (cameraStatusInfo.status == camera.CameraStatus.CAMERA_STATUS_APPEAR) {
console.info(`New Camera device appear.`)
}
// 如果当断开相机设备USB连接时,回调函数会返回相机被移除状态
if (cameraStatusInfo.status
== camera.CameraStatus.CAMERA_STATUS_DISAPPEAR) {
console.info(`Camera device has been removed.`)
}
// 相机被关闭时,回调函数会返回相机可用状态
if (cameraStatusInfo.status
== camera.CameraStatus.CAMERA_STATUS_AVAILABLE) {
console.info(`Current Camera is available.`)
}
// 相机被打开/占用时,回调函数会返回相机不可用状态
if (cameraStatusInfo.status
== camera.CameraStatus.CAMERA_STATUS_UNAVAILABLE) {
console.info(`Current Camera has been occupied.`)
}
console.info(`camera: ${cameraStatusInfo.camera.cameraId}`)
console.info(`status: ${cameraStatusInfo.status}`)
}
)
}
(三) 设备输入
相机应用可通过调用和控制相机设备,完成预览、拍照和录像等基础操作。
1. 开发步骤
- 导入camera接口,接口中提供了相机相关的属性和方法,导入方法如下。
import { camera } from '@kit.CameraKit'
import { BusinessError } from '@kit.BasicServicesKit'
2. 通过cameraManager类中的createCameraInput方法创建相机输入流。
async function createInput(
cameraDevice: camera.CameraDevice,
cameraManager: camera.CameraManager
): Promise<camera.CameraInput | undefined> {
// 创建相机输入流
let cameraInput: camera.CameraInput | undefined = undefined
try {
cameraInput = cameraManager.createCameraInput(cameraDevice);
} catch (error) {
let err = error as BusinessError
console.error('Failed to createCameraInput errorCode = ' + err.code)
}
if (cameraInput === undefined) {
return undefined
}
// 监听cameraInput错误信息
cameraInput.on('error', cameraDevice, (error: BusinessError) => {
console.error(`Camera input error code: ${error.code}`)
});
// 打开相机
await cameraInput.open()
return cameraInput
}
3. 通过getSupportedSceneModes方法,获取当前相机设备支持的模式列表,列表中存储了相机设备支持的所有模式SceneMode。
function getSupportedSceneMode(
cameraDevice: camera.CameraDevice,
cameraManager: camera.CameraManager
): Array<camera.SceneMode> {
// 获取相机设备支持的模式列表
let sceneModeArray: Array<camera.SceneMode>
= cameraManager.getSupportedSceneModes(cameraDevice)
if (sceneModeArray != undefined && sceneModeArray.length > 0) {
for (let index = 0; index < sceneModeArray.length; index++) {
console.info('Camera SceneMode : ' + sceneModeArray[index])
}
return sceneModeArray
} else {
console.error("cameraManager.getSupportedSceneModes error")
return []
}
}
4. 通过getSupportedOutputCapability方法,获取当前相机设备支持的所有输出流,如预览流、拍照流、录像流等。输出流在CameraOutputCapability中的各个profile字段中,根据相机设备指定模式SceneMode的不同,需要添加不同类型的输出流。
async function getSupportedOutputCapability(
cameraDevice: camera.CameraDevice,
cameraManager: camera.CameraManager, sceneMode: camera.SceneMode
): Promise<camera.CameraOutputCapability | undefined> {
// 获取相机设备支持的输出流能力
let cameraOutputCapability: camera.CameraOutputCapability
= cameraManager.getSupportedOutputCapability(cameraDevice, sceneMode)
if (!cameraOutputCapability) {
console.error("cameraManager.getSupportedOutputCapability error")
return undefined;
}
console.info("outputCapability: " + JSON.stringify(cameraOutputCapability))
// 以NORMAL_PHOTO模式为例,需要添加预览流、拍照流
// previewProfiles属性为获取当前设备支持的预览输出流
let previewProfilesArray: Array<camera.Profile>
= cameraOutputCapability.previewProfiles
if (!previewProfilesArray) {
console.error("createOutput previewProfilesArray == null || undefined")
}
//photoProfiles属性为获取当前设备支持的拍照输出流
let photoProfilesArray: Array<camera.Profile>
= cameraOutputCapability.photoProfiles
if (!photoProfilesArray) {
console.error("createOutput photoProfilesArray == null || undefined")
}
return cameraOutputCapability
}
(四) 会话管理
相机使用预览、拍照、录像、元数据功能前,均需要创建相机会话。
1. 开发步骤
- 导入相关接口,导入方法如下。
import { camera } from '@kit.CameraKit'
import { BusinessError } from '@kit.BasicServicesKit'
2. 调用cameraManager类中的createSession方法创建一个会话。
function getSession(
cameraManager: camera.CameraManager
): camera.Session | undefined {
let session: camera.Session | undefined = undefined
try {
session = cameraManager.createSession(
camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to create the session instance. `
+ `error: ${JSON.stringify(err)}`)
}
return session
}
3. 调用PhotoSession类中的beginConfig方法配置会话。
function beginConfig(photoSession: camera.PhotoSession): void {
try {
photoSession.beginConfig()
} catch (error) {
let err = error as BusinessError
console.error(`Failed to beginConfig. error: ${JSON.stringify(err)}`)
}
}
4. 使能。向会话中添加相机的输入流和输出流,调用addInput添加相机的输入流;调用addOutput添加相机的输出流。以下示例代码以添加预览流previewOutput和拍照流photoOutput为例,即当前模式支持拍照和预览。
调用PhotoSession类中的commitConfig和start方法提交相关配置,并启动会话。
async function startSession(
photoSession: camera.PhotoSession,
cameraInput: camera.CameraInput,
previewOutput: camera.PreviewOutput,
photoOutput: camera.PhotoOutput
): Promise<void> {
try {
photoSession.addInput(cameraInput)
} catch (error) {
let err = error as BusinessError
console.error(`Failed to addInput. error: ${JSON.stringify(err)}`)
}
try {
photoSession.addOutput(previewOutput)
} catch (error) {
let err = error as BusinessError
console.error(`Failed to add previewOutput. error: ${JSON.stringify(err)}`)
}
try {
photoSession.addOutput(photoOutput)
} catch (error) {
let err = error as BusinessError
console.error(`Failed to add photoOutput. error: ${JSON.stringify(err)}`)
}
try {
await photoSession.commitConfig()
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to commitConfig. error: ${JSON.stringify(err)}`)
}
try {
await photoSession.start()
} catch (error) {
let err = error as BusinessError
console.error(`Failed to start. error: ${JSON.stringify(err)}`)
}
}
5. 会话控制。调用PhotoSession类中的stop方法可以停止当前会话。调用removeOutput和addOutput方法可以完成会话切换控制。以下示例代码以移除拍照流photoOutput,添加视频流videoOutput为例,完成了拍照到录像的切换。
async function switchOutput(
photoSession: camera.PhotoSession,
videoOutput: camera.VideoOutput,
photoOutput: camera.PhotoOutput
): Promise<void> {
try {
await photoSession.stop()
} catch (error) {
let err = error as BusinessError
console.error(`Failed to stop. error: ${JSON.stringify(err)}`)
}
try {
photoSession.beginConfig()
} catch (error) {
let err = error as BusinessError
console.error(`Failed to beginConfig. error: ${JSON.stringify(err)}`)
}
// 从会话中移除拍照输出流
try {
photoSession.removeOutput(photoOutput)
} catch (error) {
let err = error as BusinessError
console.error(`Failed to remove photoOutput. error: ${JSON.stringify(err)}`)
}
// 向会话中添加视频输出流
try {
photoSession.addOutput(videoOutput)
} catch (error) {
let err = error as BusinessError
console.error(`Failed to add videoOutput. error: ${JSON.stringify(err)}`)
}
}
(五) 预览
预览是启动相机后看见的画面,通常在拍照和录像前执行。
1. 开发步骤
- 导入camera接口,接口中提供了相机相关的属性和方法,导入方法如下。
import { camera } from '@kit.CameraKit'
import { BusinessError } from '@kit.BasicServicesKit'
2. 创建Surface。
XComponent组件为预览流提供的Surface,获取surfaceId使用getXcomponentSurfaceId方法。
- 通过CameraOutputCapability类中的previewProfiles属性获取当前设备支持的预览能力,返回previewProfilesArray数组 。通过createPreviewOutput方法创建预览输出流,其中,createPreviewOutput方法中的两个参数分别是previewProfilesArray数组中的第一项和步骤二中获取的surfaceId。
function getPreviewOutput(
cameraManager: camera.CameraManager,
cameraOutputCapability: camera.CameraOutputCapability,
surfaceId: string
): camera.PreviewOutput | undefined {
let previewProfilesArray: Array<camera.Profile>
= cameraOutputCapability.previewProfiles
let previewOutput: camera.PreviewOutput | undefined = undefined
try {
previewOutput
= cameraManager.createPreviewOutput(previewProfilesArray[0], surfaceId)
} catch (error) {
let err = error as BusinessError;
console.error("Failed to create the PreviewOutput instance. error code: "
+ err.code)
}
return previewOutput
}
4. 使能。通过Session.start方法输出预览流,接口调用失败会返回相应错误码。
async function startPreviewOutput(
cameraManager: camera.CameraManager,
previewOutput: camera.PreviewOutput
): Promise<void> {
let cameraArray: Array<camera.CameraDevice> = []
cameraArray = cameraManager.getSupportedCameras()
if (cameraArray.length == 0) {
console.error('no camera.')
return
}
// 获取支持的模式类型
let sceneModes: Array<camera.SceneMode>
= cameraManager.getSupportedSceneModes(cameraArray[0])
let isSupportPhotoMode: boolean
= sceneModes.indexOf(camera.SceneMode.NORMAL_PHOTO) >= 0
if (!isSupportPhotoMode) {
console.error('photo mode not support')
return
}
let cameraInput: camera.CameraInput | undefined = undefined
cameraInput = cameraManager.createCameraInput(cameraArray[0])
if (cameraInput === undefined) {
console.error('cameraInput is undefined')
return
}
// 打开相机
await cameraInput.open();
let session: camera.PhotoSession = cameraManager.createSession(
camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession
session.beginConfig()
session.addInput(cameraInput)
session.addOutput(previewOutput)
await session.commitConfig()
await session.start()
}
2. 状态监听
在相机应用开发过程中,可以随时监听预览输出流状态,包括预览流启动、预览流结束、预览流输出错误。
- 通过注册固定的frameStart回调函数获取监听预览启动结果,previewOutput创建成功时即可监听,预览第一次曝光时触发,有该事件返回结果则认为预览流已启动。
function onPreviewOutputFrameStart(previewOutput: camera.PreviewOutput): void {
previewOutput.on('frameStart', (err: BusinessError) => {
if (err !== undefined && err.code !== 0) {
return
}
console.info('Preview frame started')
})
}
- 通过注册固定的frameEnd回调函数获取监听预览结束结果,previewOutput创建成功时即可监听,预览完成最后一帧时触发,有该事件返回结果则认为预览流已结束。
function onPreviewOutputFrameEnd(previewOutput: camera.PreviewOutput): void {
previewOutput.on('frameEnd', (err: BusinessError) => {
if (err !== undefined && err.code !== 0) {
return
}
console.info('Preview frame ended')
})
}
- 通过注册固定的error回调函数获取监听预览输出错误结果,回调返回预览输出接口使用错误时对应的错误码。
function onPreviewOutputError(previewOutput: camera.PreviewOutput): void {
previewOutput.on('error', (previewOutputError: BusinessError) => {
console.error(`Preview output error code: ${previewOutputError.code}`)
})
}
(六) 拍照
拍照是相机的最重要功能之一,拍照模块基于相机复杂的逻辑,为了保证用户拍出的照片质量,在中间步骤可以设置分辨率、闪光灯、焦距、照片质量及旋转角度等信息。
1. 开发步骤
- 导入image接口。创建拍照输出流的SurfaceId以及拍照输出的数据,都需要用到系统提供的image接口能力,导入image接口的方法如下。
import { image } from '@kit.ImageKit'
import { camera } from '@kit.CameraKit'
import { fileIo as fs } from '@kit.CoreFileKit'
import { photoAccessHelper } from '@kit.MediaLibraryKit'
import { BusinessError } from '@kit.BasicServicesKit'
2. 创建拍照输出流。
通过CameraOutputCapability类中的photoProfiles属性,可获取当前设备支持的拍照输出流,通过createPhotoOutput方法传入支持的某一个输出流及步骤一获取的SurfaceId创建拍照输出流。
function getPhotoOutput(
cameraManager: camera.CameraManager,
cameraOutputCapability: camera.CameraOutputCapability
): camera.PhotoOutput | undefined {
let photoProfilesArray: Array<camera.Profile>
= cameraOutputCapability.photoProfiles
if (!photoProfilesArray) {
console.error("createOutput photoProfilesArray == null || undefined")
}
let photoOutput: camera.PhotoOutput | undefined = undefined
try {
photoOutput = cameraManager.createPhotoOutput(photoProfilesArray[0])
} catch (error) {
let err = error as BusinessError
console.error(`Failed to createPhotoOutput. error: ${JSON.stringify(err)}`)
}
return photoOutput
}
3. 设置拍照photoAvailable的回调,并将拍照的buffer保存为图片。
如需要在图库中看到所保存的图片、视频资源,需要将其保存到媒体库。
需要在photoOutput.on('photoAvailable')接口获取到buffer时,将buffer在安全控件中保存到媒体库。
let context = getContext(this)
function setPhotoOutputCb(photoOutput: camera.PhotoOutput) {
//设置回调之后,调用photoOutput的capture方法,就会将拍照的buffer回传到回调中
photoOutput.on(
'photoAvailable',
(errCode: BusinessError, photo: camera.Photo): void => {
console.info('getPhoto start')
console.info(`err: ${JSON.stringify(errCode)}`)
if (errCode || photo === undefined) {
console.error('getPhoto failed')
return
}
let imageObj: image.Image = photo.main
imageObj.getComponent(
image.ComponentType.JPEG,
(errCode: BusinessError, component: image.Component): void => {
console.info('getComponent start');
if (errCode || component === undefined) {
console.error('getComponent failed')
return
}
let buffer: ArrayBuffer;
if (component.byteBuffer) {
buffer = component.byteBuffer
} else {
console.error('byteBuffer is null')
return
}
// 如需要在图库中看到所保存的图片、视频资源,请使用用户无感的安全控件创建媒体资源。
// buffer处理结束后需要释放该资源,如果未正确释放资源会导致后续拍照获取不到buffer
imageObj.release()
})
})
}
4. 参数配置。
配置相机的参数可以调整拍照的一些功能,包括闪光灯、变焦、焦距等。
function configuringSession(photoSession: camera.PhotoSession): void {
// 判断设备是否支持闪光灯
let flashStatus: boolean = false
try {
flashStatus = photoSession.hasFlash()
} catch (error) {
let err = error as BusinessError
console.error(`Failed to hasFlash. error: ${JSON.stringify(err)}`)
}
console.info(`Returned with the flash light support status: ${flashStatus}`)
if (flashStatus) {
// 判断是否支持自动闪光灯模式
let flashModeStatus: boolean = false
try {
let status: boolean
= photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_AUTO)
flashModeStatus = status
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to check whether the flash mode is supported. `
+ `error: ${JSON.stringify(err)}`)
}
if (flashModeStatus) {
// 设置自动闪光灯模式
try {
photoSession.setFlashMode(camera.FlashMode.FLASH_MODE_AUTO)
} catch (error) {
let err = error as BusinessError
console.error(`Failed to set the flash mode. `
+ `error: ${JSON.stringify(err)}`)
}
}
}
// 判断是否支持连续自动变焦模式
let focusModeStatus: boolean = false
try {
let status: boolean = photoSession.isFocusModeSupported(
camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO)
focusModeStatus = status
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to check whether the focus mode is supported. `
+ `error: ${JSON.stringify(err)}`)
}
if (focusModeStatus) {
// 设置连续自动变焦模式
try {
photoSession.setFocusMode(camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO)
} catch (error) {
let err = error as BusinessError
console.error(`Failed to set the focus mode. `
+ `error: ${JSON.stringify(err)}`)
}
}
// 获取相机支持的可变焦距比范围
let zoomRatioRange: Array<number> = []
try {
zoomRatioRange = photoSession.getZoomRatioRange()
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to get the zoom ratio range. `
+ `error: ${JSON.stringify(err)}`)
}
if (zoomRatioRange.length <= 0 ) {
return
}
// 设置可变焦距比
try {
photoSession.setZoomRatio(zoomRatioRange[0]);
} catch (error) {
let err = error as BusinessError;
console.error(`Failed to set the zoom ratio value. `
+ `error: ${JSON.stringify(err)}`)
}
}
5. 触发拍照。
通过photoOutput类的capture方法,执行拍照任务。该方法有两个参数,第一个参数为拍照设置参数的setting,setting中可以设置照片的质量和旋转角度,第二参数为回调函数。
获取拍照旋转角度的方法为,通过通过PhotoOutput类中的getPhotoRotation方法获取rotation实际的值
function capture(
captureLocation: camera.Location,
photoOutput: camera.PhotoOutput
): void {
let settings: camera.PhotoCaptureSetting = {
quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, // 设置图片质量高
// 设置图片旋转角度的camera.ImageRotation.ROTATION_0
// 是通过说明中获取拍照角度的getPhotoRotation方法获取的值进行设置
rotation: camera.ImageRotation.ROTATION_0,
location: captureLocation, // 设置图片地理位置
mirror: false // 设置镜像使能开关(默认关)
};
photoOutput.capture(settings, (err: BusinessError) => {
if (err) {
console.error(`Failed to capture the photo. `
+ `error: ${JSON.stringify(err)}`)
return
}
console.info('Callback invoked to indicate '
+ 'the photo capture request success.')
})
}
2. 状态监听
在相机应用开发过程中,可以随时监听拍照输出流状态,包括拍照流开始、拍照帧的开始与结束、拍照输出流的错误。
- 通过注册固定的captureStart回调函数获取监听拍照开始结果,photoOutput创建成功时即可监听,相机设备已经准备开始这次拍照时触发,该事件返回此次拍照的captureId。
function onPhotoOutputCaptureStart(photoOutput: camera.PhotoOutput): void {
photoOutput.on(
'captureStartWithInfo',
(err: BusinessError, captureStartInfo: camera.CaptureStartInfo) => {
if (err !== undefined && err.code !== 0) {
return
}
console.info(`photo capture started, `
+ `captureId : ${captureStartInfo.captureId}`)
})
}
- 通过注册固定的captureEnd回调函数获取监听拍照结束结果,photoOutput创建成功时即可监听,该事件返回结果为拍照完全结束后的相关信息CaptureEndInfo。
function onPhotoOutputCaptureEnd(photoOutput: camera.PhotoOutput): void {
photoOutput.on(
'captureEnd',
(err: BusinessError, captureEndInfo: camera.CaptureEndInfo) => {
if (err !== undefined && err.code !== 0) {
return
}
console.info(`photo capture end, captureId : ${captureEndInfo.captureId}`)
console.info(`frameCount : ${captureEndInfo.frameCount}`)
})
}
- 通过注册固定的captureReady回调函数获取监听可拍下一张结果,photoOutput创建成功时即可监听,当下一张可拍时触发,该事件返回结果为下一张可拍的相关信息。
function onPhotoOutputCaptureReady(photoOutput: camera.PhotoOutput): void {
photoOutput.on('captureReady', (err: BusinessError) => {
if (err !== undefined && err.code !== 0) {
return
}
console.info(`photo capture ready`)
})
}
- 通过注册固定的error回调函数获取监听拍照输出流的错误结果。回调返回拍照输出接口使用错误时的对应错误码。
function onPhotoOutputError(photoOutput: camera.PhotoOutput): void {
photoOutput.on('error', (error: BusinessError) => {
console.error(`Photo output error code: ${error.code}`)
})
}
(七) 录像
录像也是相机应用的最重要功能之一,录像是循环帧的捕获。对于录像的流畅度,我们可以参考拍照中的步骤4,设置分辨率、闪光灯、焦距、照片质量及旋转角度等信息。
1. 开发步骤
- 导入media模块。
创建录像输出流的SurfaceId以及录像输出的数据,都需要用到系统提供的media接口能力,导入media接口的方法如下。
import { BusinessError } from '@kit.BasicServicesKit'
import { camera } from '@kit.CameraKit'
import { media } from '@kit.MediaKit'
2. 创建Surface。
系统提供的media接口可以创建一个录像AVRecorder实例,通过该实例的getInputSurface方法获取SurfaceId,与录像输出流做关联,处理录像输出流输出的数据。
async function getVideoSurfaceId(
aVRecorderConfig: media.AVRecorderConfig
): Promise<string | undefined> {
let avRecorder: media.AVRecorder | undefined = undefined
try {
avRecorder = await media.createAVRecorder()
} catch (error) {
let err = error as BusinessError
console.error(`createAVRecorder call failed. error code: ${err.code}`)
}
if (avRecorder === undefined) {
return undefined
}
avRecorder.prepare(aVRecorderConfig, (err: BusinessError) => {
if (err == null) {
console.info('prepare success')
} else {
console.error('prepare failed and error is ' + err.message
}
})
let videoSurfaceId = await avRecorder.getInputSurface()
return videoSurfaceId
}
3. 创建录像输出流。
通过CameraOutputCapability类中的videoProfiles属性,可获取当前设备支持的录像输出流。然后,定义创建录像的参数,通过createVideoOutput方法创建录像输出流。
获取录像旋转角度的方法:通过VideoOutput类中的getVideoRotation方法获取rotation实际的值
async function getVideoOutput(
cameraManager: camera.CameraManager,
videoSurfaceId: string,
cameraOutputCapability: camera.CameraOutputCapability
): Promise<camera.VideoOutput | undefined> {
let videoProfilesArray: Array<camera.VideoProfile>
= cameraOutputCapability.videoProfiles
if (!videoProfilesArray) {
console.error("createOutput videoProfilesArray == null || undefined")
return undefined
}
// AVRecorderProfile
let aVRecorderProfile: media.AVRecorderProfile = {
fileFormat : media.ContainerFormatType.CFT_MPEG_4, // 视频文件封装格式,只支持MP4
videoBitrate : 100000, // 视频比特率
videoCodec : media.CodecMimeType.VIDEO_AVC, // 视频文件编码格式,支持avc格式
videoFrameWidth : 640, // 视频分辨率的宽
videoFrameHeight : 480, // 视频分辨率的高
videoFrameRate : 30 // 视频帧率
};
// 创建视频录制的参数,预览流与录像输出流的分辨率的宽(videoFrameWidth)、
// 高(videoFrameHeight)比要保持一致
let aVRecorderConfig: media.AVRecorderConfig = {
videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV,
profile: aVRecorderProfile,
url: 'fd://35',
rotation: 90 // rotation的值90,是通过getPhotoRotation接口获取到的值
};
// 创建avRecorder
let avRecorder: media.AVRecorder | undefined = undefined
try {
avRecorder = await media.createAVRecorder()
} catch (error) {
let err = error as BusinessError
console.error(`createAVRecorder call failed. error code: ${err.code}`)
}
if (avRecorder === undefined) {
return undefined
}
// 设置视频录制的参数
avRecorder.prepare(aVRecorderConfig)
// 创建VideoOutput对象
let videoOutput: camera.VideoOutput | undefined = undefined
// createVideoOutput传入的videoProfile对象的宽高需要和aVRecorderProfile保持一致。
let videoProfile: undefined | camera.VideoProfile
= videoProfilesArray.find((profile: camera.VideoProfile) => {
return profile.size.width === aVRecorderProfile.videoFrameWidth
&& profile.size.height === aVRecorderProfile.videoFrameHeight
})
if (!videoProfile) {
console.error('videoProfile is not found')
return
}
try {
videoOutput = cameraManager.createVideoOutput(videoProfile, videoSurfaceId)
} catch (error) {
let err = error as BusinessError;
console.error('Failed to create the videoOutput instance. errorCode = '
+ err.code)
}
return videoOutput
}
4. 开始录像。
先通过videoOutput的start方法启动录像输出流,再通过avRecorder的start方法开始录像。
async function startVideo(
videoOutput: camera.VideoOutput,
avRecorder: media.AVRecorder
): Promise<void> {
videoOutput.start(async (err: BusinessError) => {
if (err) {
console.error(`Failed to start the video output ${err.message}`)
return;
}
console.info('Callback invoked to indicate the video output start success.')
})
try {
await avRecorder.start()
} catch (error) {
let err = error as BusinessError
console.error(`avRecorder start error: ${JSON.stringify(err)}`)
}
}
5. 停止录像。
先通过avRecorder的stop方法停止录像,再通过videoOutput的stop方法停止录像输出流。
async function stopVideo(
videoOutput: camera.VideoOutput,
avRecorder: media.AVRecorder
): Promise<void> {
try {
await avRecorder.stop()
} catch (error) {
let err = error as BusinessError
console.error(`avRecorder stop error: ${JSON.stringify(err)}`)
}
videoOutput.stop((err: BusinessError) => {
if (err) {
console.error(`Failed to stop the video output ${err.message}`)
return
}
console.info('Callback invoked to indicate the video output stop success.')
})
}
2. 状态监听
在相机应用开发过程中,可以随时监听录像输出流状态,包括录像开始、录像结束、录像流输出的错误。
- 通过注册固定的frameStart回调函数获取监听录像开始结果,videoOutput创建成功时即可监听,录像第一次曝光时触发,有该事件返回结果则认为录像开始。
function onVideoOutputFrameStart(videoOutput: camera.VideoOutput): void {
videoOutput.on('frameStart', (err: BusinessError) => {
if (err !== undefined && err.code !== 0) {
return
}
console.info('Video frame started')
})
}
- 通过注册固定的frameEnd回调函数获取监听录像结束结果,videoOutput创建成功时即可监听,录像完成最后一帧时触发,有该事件返回结果则认为录像流已结束。
function onVideoOutputFrameEnd(videoOutput: camera.VideoOutput): void {
videoOutput.on('frameEnd', (err: BusinessError) => {
if (err !== undefined && err.code !== 0) {
return
}
console.info('Video frame ended')
})
}
- 通过注册固定的error回调函数获取监听录像输出错误结果,callback返回预览输出接口使用错误时对应的错误码。
function onVideoOutputError(videoOutput: camera.VideoOutput): void {
videoOutput.on('error', (error: BusinessError) => {
console.error(`Video output error code: ${error.code}`)
})
}
✋ 需要参加鸿蒙认证的请点击 鸿蒙认证链接