【HarmonyOS】鸿蒙应用实现屏幕录制详解和源码
一、前言
官方文档关于屏幕录制的API和示例介绍获取简单和突兀。使用其他会让上手程度变高。所以特意开篇文章,讲解屏幕录制的使用。官方文档参见:使用AVScreenCaptureRecorder录屏写文件(ArkTS)
二、方案思路
鸿蒙应用关于录制屏幕,官方提供了AVScreenCaptureRecorder进行屏幕录制的调用。分为以下几个步骤: 1.创建该对象
import media from '@ohos.multimedia.media';
private avScreenCaptureRecorder: media.AVScreenCaptureRecorder | undefined = undefined;
this.avScreenCaptureRecorder = await media.createAVScreenCaptureRecorder();
2.进行属性配置初始化 这里尤其要注意,config配置属性对象的作用范围,在官方示例中,一般不喜欢创建成局部对象,而是全局对象。但是fd又是异步获取,就容器造成fd拿到后,并没有赋值给config中,导致init函数初始化一直报错401参数错误。
如果像官方示例列为全局对象,那fd的file对象也需要创建为全局对象,看起来就很恶心。所以我这里改成局部对象。
【官方DEMO关于fd的出处并没有写全,春秋笔法过多。所以我经常吐槽说官方文档基本上属于你会了才能看懂了。。】
let context = getContext(this) as common.UIAbilityContext; // 获取设备A的UIAbilityContext信息
// 沙箱路径
let pathDir: string = context.filesDir; // /data/storage/el2/base/haps/entry/files
// 视频文件名字和路径
let filesUri: string = pathDir + '/Screen_' + new Date().getTime() + '.mp4';
// 缓存Uri,用于保存媒体库使用
this.targetFileUri = filesUri;
// 创建文件,赋予写权限
let curFile = fs.openSync(filesUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let avCaptureConfig: media.AVScreenCaptureRecordConfig = {
// 文件需要先有调用者创建,赋予写权限,将文件fd传给此参数
fd: curFile.fd,
// 除了fd,其他参数都是可选,可以不设置。默认宽高就是手机时机宽高。
// frameWidth: 768,
// frameHeight: 1280,
}
await this.avScreenCaptureRecorder?.init(avCaptureConfig);
此时录屏文件是保存在我们创建的沙箱路径中的。所以并不需要官方文档中提到的读写权限。
3.然后调用开始录屏或者结束录屏。
await this.avScreenCaptureRecorder.startRecording()
await this.avScreenCaptureRecorder.stopRecording()
4.选配-录音权限的配置和申请
如果没有配置和申请录音权限。默认录屏是没有麦克风的声音。反之,录屏时你说话,就能录入到视频中。
/**
* 申请麦克风权限
*/
private questMicPermissions(){
const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
try {
atManager.requestPermissionsFromUser(getContext(), ["ohos.permission.MICROPHONE"]).then((data) => {
if (data.authResults[0] === 0) {
} else {
console.log(this.TAG, "user rejected")
}
}).catch((err: BusinessError) => {
console.log(this.TAG, "BusinessError err: " + JSON.stringify(err))
})
} catch (err) {
console.log(this.TAG, "catch err: " + JSON.stringify(err))
}
}
5.选配-将沙箱路径下的录屏保存到相册中 保存到媒体库中,有很多种方式。我此处举例使用的是saveButton的形式进行保存函数的调用。
直接调用以下保存函数是不会生效。在鸿蒙中,一定需要用户知情同意,才能将沙箱的资源保存到媒体库中。
/**
* 保存视频到媒体库
*/
private saveVideo() {
let titleStr = 'Screen_'+ new Date().getTime()
let context = getContext(this);
let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
let photoType: photoAccessHelper.PhotoType = photoAccessHelper.PhotoType.VIDEO;
let extension:string = 'mp4';
let options: photoAccessHelper.CreateOptions = {
title:titleStr
}
phAccessHelper.createAsset(photoType, extension, options).then(async (uriDes:string)=>{
try {
let file_uri = fs.openSync(this.targetFileUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let file = fs.openSync(uriDes, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.copyFileSync(file_uri.fd, file.fd);
fs.closeSync(file.fd);
fs.closeSync(file_uri.fd);
promptAction.showToast({
message: '已保存至相册!',
duration: 3000
});
}catch (err) {
console.error("error is "+ JSON.stringify(err))
}
}).catch((err:Error)=>{
console.error("error is "+ JSON.stringify(err))
});
}
SaveButton,隐私窗口的豁免和录制状态的回调监听,参见源码示例。
注意:实际开发中因为鸿蒙的后台特性,当录屏时应用切到后台大于三秒,应用进程就会被挂起。所以需要设置后台任务的长时任务。保证录屏的正常。(后面我会针对长时任务以录屏来举例,此处先不处理。)
三、源码示例:
// 申请麦克风
{
"name": "ohos.permission.MICROPHONE",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"EntryAbility"
],
"when": "always"
}
},
SCRecordTestPage.ets
import { media } from '@kit.MediaKit'
import { BusinessError } from '@kit.BasicServicesKit'
import fs from '@ohos.file.fs';
import { abilityAccessCtrl, common, PermissionRequestResult, Permissions } from '@kit.AbilityKit'
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { promptAction } from '@kit.ArkUI';
import { fileUri } from '@kit.CoreFileKit';
@Entry
@Component
struct SCRecordTestPage {
private TAG: string = "SCRecordTestPage";
private avScreenCaptureRecorder: media.AVScreenCaptureRecorder | undefined = undefined;
private targetFileUri: string = "";
private saveButtonOptions: SaveButtonOptions = {
icon: SaveIconStyle.FULL_FILLED,
text: SaveDescription.SAVE_FILE,
buttonType: ButtonType.Capsule
} // 设置安全控件按钮属性
async aboutToAppear() {
// 初始化屏幕录制渲染对象
await this.createAVScreenCapture();
}
async createAVScreenCapture() {
this.avScreenCaptureRecorder = await media.createAVScreenCaptureRecorder();
this.avScreenCaptureRecorder.on('stateChange', async (infoType: media.AVScreenCaptureStateCode) => {
switch (infoType) {
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_STARTED:
console.info("录屏成功开始后会收到的回调");
break;
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_CANCELED:
this.avScreenCaptureRecorder?.release();
this.avScreenCaptureRecorder = undefined;
console.info("不允许使用录屏功能");
break;
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_STOPPED_BY_USER:
this.avScreenCaptureRecorder?.release();
this.avScreenCaptureRecorder = undefined;
console.info("通过录屏胶囊结束录屏,底层录制会停止");
break;
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_INTERRUPTED_BY_OTHER:
console.info("录屏因其他中断而停止,底层录制会停止");
break;
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_STOPPED_BY_CALL:
console.info("录屏过程因通话中断,底层录制会停止");
break;
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_MIC_UNAVAILABLE:
console.info("录屏麦克风不可用");
break;
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_MIC_MUTED_BY_USER:
console.info("录屏麦克风被用户静音");
break;
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_MIC_UNMUTED_BY_USER:
console.info("录屏麦克风被用户取消静音");
break;
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_ENTER_PRIVATE_SCENE:
// 目前可以从系统直接注册监听到进入隐私场景
console.info("录屏进入隐私场景");
break;
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_EXIT_PRIVATE_SCENE:
console.info("录屏退出隐私场景");
break;
case media.AVScreenCaptureStateCode.SCREENCAPTURE_STATE_STOPPED_BY_USER_SWITCHES:
console.info("用户账号切换,底层录制会停止");
break;
default:
break;
}
})
this.avScreenCaptureRecorder.on('error', (err) => {
console.info("处理异常情况");
})
let context = getContext(this) as common.UIAbilityContext; // 获取设备A的UIAbilityContext信息
// 沙箱路径
let pathDir: string = context.filesDir; // /data/storage/el2/base/haps/entry/files
// 视频文件名字和路径
let filesUri: string = pathDir + '/Screen_' + new Date().getTime() + '.mp4';
// 缓存Uri,用于保存媒体库使用
this.targetFileUri = filesUri;
// 创建文件,赋予写权限
let curFile = fs.openSync(filesUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let avCaptureConfig: media.AVScreenCaptureRecordConfig = {
// 文件需要先有调用者创建,赋予写权限,将文件fd传给此参数
fd: curFile.fd,
// 除了fd,其他参数都是可选,可以不设置。默认宽高就是手机时机宽高。
// frameWidth: 768,
// frameHeight: 1280,
}
await this.avScreenCaptureRecorder?.init(avCaptureConfig);
}
build() {
Column({ space: 50 }) {
Button('选配-开启麦克风')
.onClick(() => {
this.questMicPermissions();
})
.height(60)
.width('100%')
Button('开始录屏')
.onClick(() => {
this.startRecord()
})
.height(60)
.width('100%')
Button('结束录屏')
.onClick(() => {
this.stopRecord()
})
.height(60)
.width('100%')
SaveButton(this.saveButtonOptions) // 创建安全控件按钮
.onClick(async (event, result: SaveButtonOnClickResult) => {
if (result == SaveButtonOnClickResult.SUCCESS) {
try {
this.saveVideo();
} catch (err) {
console.error(`create asset failed with error: ${err.code}, ${err.message}`);
}
} else {
console.error('SaveButtonOnClickResult create asset failed');
}
})
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
.padding({ left: 30, right: 30})
}
/**
* 申请麦克风权限
*/
private questMicPermissions(){
const atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
try {
atManager.requestPermissionsFromUser(getContext(), ["ohos.permission.MICROPHONE"]).then((data) => {
if (data.authResults[0] === 0) {
} else {
console.log(this.TAG, "user rejected")
}
}).catch((err: BusinessError) => {
console.log(this.TAG, "BusinessError err: " + JSON.stringify(err))
})
} catch (err) {
console.log(this.TAG, "catch err: " + JSON.stringify(err))
}
}
/**
* 开启录制
*/
private startRecord() {
// 创建豁免隐私窗口,这里填写的是子窗口id和主窗口id
// let windowIDs = [57, 86];
// await this.avScreenCaptureRecorder?.skipPrivacyMode(windowIDs);
this.avScreenCaptureRecorder?.startRecording().then(() => {
console.info('Succeeded in starting avScreenCaptureRecorder');
}).catch((err: BusinessError) => {
console.info('Failed to start avScreenCaptureRecorder, error: ' + err.message);
})
}
/**
* 暂停录制
*/
private stopRecord() {
this.avScreenCaptureRecorder?.stopRecording().then(() => {
console.info('Succeeded in stopping avScreenCaptureRecorder');
}).catch((err: BusinessError) => {
console.info('Failed to stop avScreenCaptureRecorder, error: ' + err.message);
})
}
/**
* 保存视频到媒体库
*/
private saveVideo() {
let titleStr = 'Screen_'+ new Date().getTime()
let context = getContext(this);
let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
let photoType: photoAccessHelper.PhotoType = photoAccessHelper.PhotoType.VIDEO;
let extension:string = 'mp4';
let options: photoAccessHelper.CreateOptions = {
title:titleStr
}
phAccessHelper.createAsset(photoType, extension, options).then(async (uriDes:string)=>{
try {
let file_uri = fs.openSync(this.targetFileUri, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let file = fs.openSync(uriDes, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
fs.copyFileSync(file_uri.fd, file.fd);
fs.closeSync(file.fd);
fs.closeSync(file_uri.fd);
promptAction.showToast({
message: '已保存至相册!',
duration: 3000
});
}catch (err) {
console.error("error is "+ JSON.stringify(err))
}
}).catch((err:Error)=>{
console.error("error is "+ JSON.stringify(err))
});
}
}