HarmonyOS Next 一文搞懂图片和视频选择

背景

在聊天软件中,发送相册中视频和照片、用相机拍摄视频和图片发送是很常用的功能。在Android和iOS端,大部分应用都通过API方式定义UI来实现相册选择照片、视频,相机拍摄照片、视频,它们一般都支持以下功能:

  1. 相册选择:

    1. 支持单选或多选;

    2. 对图片支持是否原图选择;

    3. 对于视频支持选择视频的文件大小、视频时长等过滤;

    4. 支持点击图像放大预览

  2. 对于相机拍摄

    1. 支持点击拍照,长按录制视频;

    2. 视频录制支持最大最小录制时长限制;

    3. 拍摄或录制结束后支持预览。

06d30d20438161f5207e1f5876839ac7.jpeg

1b37acde28e9edbeb2a7dbac8a1e28f7.jpeg对于鸿蒙应用要实现上述功能,系统也提供了对应API,要实现上述功能需要几个系统权限:

  • 读取系统相册权限

  • 麦克风权限

  • 摄像头权限

HarmonyOS 权限系统介绍

与Android系统相比,HarmonyOS提供了更严谨的权限控制,这里不得不提HarmonyOS的应用权限管控策略。HarmonyOS 提供了一种允许应用访问系统资源(如:通讯录等)和系统能力(如:访问摄像头、麦克风等)的通用权限访问方式,来保护系统数据(包括用户个人数据)或功能,避免它们被不当或恶意使用,应用权限保护的对象可以分为数据和功能:

  • 数据包括个人数据(如照片、通讯录、日历、位置等)、设备数据(如设备标识、相机、麦克风等)。

  • 功能包括设备功能(如访问摄像头/麦克风、打电话、联网等)、应用功能(如弹出悬浮窗、创建快捷方式等)。

同时HarmonyOS 根据授权方式的不同,权限类型可分为system_grant(系统授权)和user_grant(用户授权):

  • system_grant(系统授权)指的是系统授权类型,在该类型的权限许可下,应用被允许访问的数据不会涉及到用户或设备的敏感信息,应用被允许执行的操作对系统或者其他应用产生的影响可控。如果在应用中申请了system_grant权限,那么系统会在用户安装应用时,自动把相应权限授予给应用。

  • user_grant(用户授权)指的是用户授权类型,在该类型的权限许可下,应用被允许访问的数据将会涉及到用户或设备的敏感信息,应用被允许执行的操作可能对系统或者其他应用产生严重的影响。该类型权限不仅需要在安装包中申请权限,还需要在应用动态运行时,通过发送弹窗的方式请求用户授权。在用户手动允许授权后,应用才会真正获取相应权限,从而成功访问操作目标对象。

例如,在应用权限列表中,麦克风和摄像头对应的权限都是属于用户授权权限,列表中给出了详细的权限使用理由。应用需要在应用商店的详情页面,向用户展示所申请的user_grant权限列表。

除了授权方式外还要了解另一个概念APL(Ability Privilege Level,元能力权限等级)等级。应用的等级可以分为以下三个等级,等级依次提高。

APL级别说明
normal默认情况下,应用的APL等级都为normal等级。
system_basic该等级的应用服务提供系统基础服务。
system_core该等级的应用服务提供操作系统核心能力。
应用APL等级不允许配置为system_core。

根据权限对于不同等级应用有不同的开放范围,权限类型对应分为以下三个等级,等级依次提高。

APL级别说明开放范围
normal允许应用访问超出默认规则外的普通系统资源,如配置Wi-Fi信息、调用相机拍摄等。这些系统资源的开放(包括数据和功能)对用户隐私以及其他应用带来的风险低。APL等级为normal及以上的应用。
system_basic允许应用访问操作系统基础服务(系统提供或者预置的基础功能)相关的资源,如系统设置、身份认证等。这些系统资源的开放对用户隐私以及其他应用带来的风险较高。1、APL等级为system_basic及以上的应用。2、部分权限对normal级别的应用受限开放,这部分权限在本指导中描述为“受限开放权限”。
system_core涉及开放操作系统核心资源的访问操作。这部分系统资源是系统最核心的底层服务,如果遭受破坏,操作系统将无法正常运行。1、 APL等级为system_core的应用。2、 仅对系统应用开放。
了解了授权方式和权限等级后,我们再来聊聊这个设计背后的原因。

首先聊聊授权方式,其中系统授权类似于Android中的普通权限申请,在清单文件声明即可,对于用户授权,类似于Android中的动态权限,需要触发弹窗让用户感知后决定是否授权。这个涉及背后的逻辑很清晰,对于数据不会涉及到用户或设备的敏感信息(比如使用网络、使用蓝牙等),默认声明后授权不会对系统产生什么破坏,如果也强制用户感知授权后才可以使用,对于开发者来说增加工作量,对用户体验也不是很友好;

其次,对于权限等级,HarmonyOS分成了三类,normal 级别的,即普通应用应用可以使用的权限,比如相机,麦克风等,只要说明使用的场景即可向用户申请;system_core级别的是只有系统应用可以使用,普通应用无法申请,如果普通应用使用可能会对系统造成不可修复的错误,比如类似Android的root权限,如果普通应用申请到后,删除了系统核心文件等,系统会遭到破坏;第三种是system_basic,主要对系统应用开发,对普通应用受限开放,什么是受限开放呢?比如说读取相册权限,如果授权给普通应用,用户赋予它这个权限后,它偷偷的将相册数据传送到应用服务端用户完全无法感知,但是对于一些应用,比如网盘、相册备份类应用,没有这个权限还无法工作,所以系统设计时考虑到这一点,对于特殊的应用向平台申请后,平台根据应用类型给特殊应用放开该权限。可能有人会疑惑,为什么读取相册的是受限,而相机、麦克风是normal权限,因为麦克风、相机无法在用户无感知时使用,而读取文件可以。

回到正题,选择图片视频需要system_basic 受限权限,麦克风相机也需要用户授权,为了减少授权导致的操作流程终端,HarmonyOS 提供了系统Picker,可以使用系统组件再不需要权限的情况下完成功能。 应用拉起系统Picker组件(文件选择器、照片选择器、联系人选择器等),由用户在Picker上选择对应的文件、照片、联系人等资源,应用即可获取到Picker的返回结果,系统Picker由系统独立进程实现,从系统Picker获取资源是一个跨进程调度过程。

为什么使用系统Picker获取资源不需要权限?因为从系统Picker获取资源需要用户操作,是用户可以感知的,所以不需要再申请权限。注意,此时应用获取到的读取资源的权限是临时的,受限的,只能临时受限访问对应的资源。我们从Picker里选择了1.jpeg,在我们应用进程可以操作这个文件,我们从系统相册中获取到另一个2.jpeg文件,在进程里直接访问是不可以的,而且对1.jpeg的访问也是有时效性的。

接下来介绍如何基于系统API,从相册选择照片和视频,以及从相机拍摄视频和照片。

从相册选择

7dfe90ea6200fa1e299dabcb3f99e39a.jpegHarmonyOS 提供了 photoAccessHelper.PhotoViewPicker() 获取系统相册中的图片,下面是使用PhotoViewPicker的示例:

import { BusinessError } from '@kit.BasicServicesKit';
async function example01() {
  try {
    let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
    PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
    PhotoSelectOptions.maxSelectNumber = 5;
    
    let photoPicker = new photoAccessHelper.PhotoViewPicker();
    photoPicker.select(PhotoSelectOptions).then((PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
      console.info('PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(PhotoSelectResult));
    }).catch((err: BusinessError) => {
      console.error(`PhotoViewPicker.select failed with err: ${err.code}, ${err.message}`);
    });
  } catch (error) {
    let err: BusinessError = error as BusinessError;
    console.error(`PhotoViewPicker failed with err: ${err.code}, ${err.message}`);
  }
}

这里面涉及到两个对象PhotoSelectOptions和PhotoSelectResult,分别表示跳转到图片选择器的参数,和选择完返回的结果。 PhotoSelectOptions有一下三个参数:

名称类型必填说明
isEditSupported11+boolean是否支持编辑照片,true表示支持,false表示不支持,默认为true。
isOriginalSupported12+boolean是否显示选择原图按钮,true表示显示,false表示不显示,默认为true。

元服务API: 从API version 12开始,该接口支持在元服务中使用。
subWindowName12+string子窗窗口名称。

元服务API: 从API version 12开始,该接口支持在元服务中使用。

PhotoSelectOptions 继承自BaseSelectOptions,提供了以下配置:

名称类型必填说明
MIMEType10+PhotoViewMIMETypes可选择的媒体文件类型,若无此参数,则默认为图片和视频类型。

元服务API: 从API version 11开始,该接口支持在元服务中使用。
maxSelectNumber10+number选择媒体文件数量的最大值(最大可设置的值为500,若不设置则默认为50)。

元服务API: 从API version 11开始,该接口支持在元服务中使用。
isPhotoTakingSupported11+boolean是否支持拍照,true表示支持,false表示不支持,默认为true。

元服务API: 从API version 11开始,该接口支持在元服务中使用。
isSearchSupported11+boolean是否支持搜索,true表示支持,false表示不支持,默认为true。

元服务API: 从API version 11开始,该接口支持在元服务中使用。
recommendationOptions11+RecommendationOptions图片推荐相关配置参数。

元服务API: 从API version 11开始,该接口支持在元服务中使用。
preselectedUris11+Array预选择图片的uri数据。

元服务API: 从API version 11开始,该接口支持在元服务中使用。
isPreviewForSingleSelectionSupported12+boolean单选模式下是否需要进大图预览,true表示需要,false表示不需要,默认为true。

元服务API: 从API version 12开始,该接口支持在元服务中使用。

综合上述配置说明,通过PhotoViewPicker可以实现最开始我们提到的:

  1. 支持选择图片和视频

  2. 支持设置最多选择媒体数量

  3. 支持是否选择原图 不支持选择视频的最大最小长度配置。

PhotoSelectResult是返回图库选择后的结果集,结构如下:

名称类型可读可写说明
photoUrisArray返回图库选择后的媒体文件的uri数组,此uri数组只能通过临时授权的方式调用photoAccessHelper.getAssets接口去使用,具体使用方式参见用户文件uri介绍中的媒体文件uri的使用方式。
isOriginalPhotoboolean返回图库选择后的媒体文件是否为原图。

返回用户选中的媒体列表和是否选中原图。这里产生一个问题,如果既有选择视频又有选择图片,如何根据一个uri判断是视频还是图片? 这里有用到photoAccessHelper 来解析媒体信息,下面是一个方法封装:

public async uriGetAssets(uri:string): Promise<photoAccessHelper.PhotoAsset|undefined> {  
  try {  
    let context = getContext(this) as common.UIAbilityContext;  
    let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);  
    let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();  
    // 配置查询条件,使用PhotoViewPicker选择图片返回的uri进行查询  
    predicates.equalTo('uri', uri);  
    let fetchOption: photoAccessHelper.FetchOptions = {  
      fetchColumns: [photoAccessHelper.PhotoKeys.WIDTH, photoAccessHelper.PhotoKeys.HEIGHT, photoAccessHelper.PhotoKeys.TITLE, photoAccessHelper.PhotoKeys.DURATION],  
      predicates: predicates  
    };  
    let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> = await phAccessHelper.getAssets(fetchOption);  
    // 得到uri对应的PhotoAsset对象,读取文件的部分信息  
    const asset: photoAccessHelper.PhotoAsset = await fetchResult.getFirstObject();  
    Logg.i(TAG, 'asset displayName: ' + asset.displayName);  
    Logg.i(TAG, 'asset uri: '+asset.uri);  
    Logg.i(TAG, 'asset photoType: ' + asset.photoType);  
    Logg.i(TAG, 'asset width: ' +  asset.get(photoAccessHelper.PhotoKeys.WIDTH));  
    Logg.i(TAG, 'asset height: ' +  asset.get(photoAccessHelper.PhotoKeys.HEIGHT));  
    Logg.i(TAG, 'asset title: '  +  asset.get(photoAccessHelper.PhotoKeys.TITLE));  
    return asset;  
  } catch (error){  
    Logg.e(TAG, 'uriGetAssets failed with err: ' + JSON.stringify(error));  
  }  
  return undefined;  
}

FetchOptions用来配置要查询的媒体信息内容,比如长、宽等信息,其中photoAccessHelper.PhotoAsset的photoType表示媒体类型,是音频还是视频。

接下里我们再介绍另一个能力,如果是视频的话有时候想要获取视频封面,即Thumbnail,photoAccessHelper.PhotoAsset提供了查询封面的接口:getThumbnail(),返回的是一个pixelMap,如何将pixelMap转换成图像呢?在pixelMap文档里看到从pixelMap读取buffer的方法:

import { BusinessError } from '@kit.BasicServicesKit';
async function Demo() {
  const readBuffer: ArrayBuffer = new ArrayBuffer(96); // 96为需要创建的像素buffer大小,取值为:height * width *4
  if (pixelMap != undefined) {
    pixelMap.readPixelsToBuffer(readBuffer, (error: BusinessError, res: void) => {
      if(error) {
        console.error(`Failed to read image pixel data. code is ${error.code}, message is ${error.message}`);// 不符合条件则进入
        return;
      } else {
        console.info('Succeeded in reading image pixel data.');  //符合条件则进入
      }
    })
  }
}

上面ArrayBuffer需要指定大小,将前面获取到的宽高相乘再乘以4即可。将获取到的readBuffer写入文件发现图片不是正常的图片,这里又需要用到image.createImagePacker():

let imagePath:string|undefined = undefined;  
let pixelMap = await matedata.getThumbnail();  
Logg.i(this.TAG, 'getThumbnail successful ' + JSON.stringify(pixelMap));  
let cacheDir = getContext().cacheDir;  
imagePath = `${cacheDir}/thumbnail${Date.now()}.jpg`;  
let dstFile = fs.openSync(imagePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);  
let packOpts: image.PackingOption = { format: "image/jpeg", quality: 98 };  
await image.createImagePacker().packToFile(pixelMap, dstFile.fd, packOpts)  
fs.close(dstFile);  
Logg.i(this.TAG, "copy file success");

需用用createImagePacker将pixelMap写入到目标文件,并且指定图片格式和质量。

从相机拍摄

6c94f12ccd975641f49c98f123f3ff5b.jpeg

6d433890cfb7b9eda43a130aa8280f95.jpeg上面实现了从相册获取照片,接下来实现使用摄像机拍照和拍视频功能。 有两种方式可以实现打开系统相机进行拍摄。

Want

下面是示例代码:

invokeCamera(callback: (uri: string) => void) {  
  const context = getContext(this) as common.UIAbilityContext;  
  const want: Want = {  
    action: 'ohos.want.action.imageCapture',  
    parameters: {  
      "callBundleName": context.abilityInfo.bundleName,  
    }  
  };  
  const result: (error: BusinessError, data: common.AbilityResult) => void =  
    (error: BusinessError, data: common.AbilityResult) => {  
      if (error && error.code !== 0) {  
        console.log(`${TAG_CAMERA_ERROR} ${JSON.stringify(error.message)}`)  
        return;  
      }  
      // 获取相机拍照后返回的图片地址  
      const resultUri: string = data.want?.parameters?.resourceUri as string;  
      if (callback && resultUri) {  
        callback(resultUri);  
      }  
    }  
  context.startAbilityForResult(want, result);  
}

通过want意图对象打开相机界面,参考 如何调用系统拍照并获取图片,这种方式无法设置录制视频最大长度等。

cameraPicker

相机选择器使用示例:

import { cameraPicker as picker } from '@kit.CameraKit';
import { camera } from '@kit.CameraKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
let mContext = getContext(this) as common.Context;


async function demo() {
  try {
    let pickerProfile: picker.PickerProfile = {
      cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
    };
    let pickerResult: picker.PickerResult = await picker.pick(mContext,
      [picker.PickerMediaType.PHOTO, picker.PickerMediaType.VIDEO], pickerProfile);
    console.log("the pick pickerResult is:" + JSON.stringify(pickerResult));
  } catch (error) {
    let err = error as BusinessError;
    console.error(`the pick call failed. error code: ${err.code}`);
  }
}

示例中mediaTypes数组指定了媒体格式,可以包含视频和图片;PickerProfile指定了摄像头是前置还是后置,主要有一下属性:

名称类型必填说明
cameraPositioncamera.CameraPosition相机的位置。
saveUristring保存配置信息的uri。
videoDurationnumber录制的最大时长。
PickerResult是相机选择器的处理结果,有一下属性:


名称类型必填说明
resultCodenumber处理的结果,成功返回0,失败返回-1。
resultUristring返回的uri地址。若saveUri为空,resultUri为公共媒体路径。若saveUri不为空且具备写权限,resultUri与saveUri相同。若saveUri不为空且不具备写权限,则无法获取到resultUri。
mediaTypePickerMediaType返回的媒体类型。
根据返回结果中mediaType来表示是图片还是视频,按照不同媒体类型处理即可。


总结

本文介绍了HarmonyOS Next中图片视频选择、图片视频拍摄的能力,重点分析了无需权限即可实现的photoAccessHelper和cameraPicker组件。

作者:轻口味
链接:https://juejin.cn/post/7419185467708735515

关注我获取更多知识或者投稿

fda7a1b95bbb15dcbc853c3f3b5c595d.jpeg

073155c173854a9bf2dcb544397d2dc8.jpeg

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值