背景
在聊天軟件中,發送相冊中視頻和照片、用相機拍攝視頻和圖片發送是很常用的功能。在Android和iOS端,大部分應用都通過API方式定義UI來實現相冊選擇照片、視頻,相機拍攝照片、視頻,它們一般都支持以下功能:
- 相冊選擇:
- 支持單選或多選;
- 對圖片支持是否原圖選擇;
- 對于視頻支持選擇視頻的文件大小、視頻時長等過濾;
- 支持點擊圖像放大預覽
- 對于相機拍攝
- 支持點擊拍照,長按錄制視頻;
- 視頻錄制支持最大最小錄制時長限制;
- 拍攝或錄制結束后支持預覽。
對于鴻蒙應用要實現上述功能,系統也提供了對應API,要實現上述功能需要幾個系統權限:
3. 讀取系統相冊權限
4. 麥克風權限
5. 攝像頭權限
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、 僅對系統應用開放。 |
訪問操作系統基礎服務(系統提供或者預置的基礎功能)相關的資源,如系統設置、身份認證等。這些系統資源的開放對用戶隱私以及其他應用帶來的風險較高。?? ?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,從相冊選擇照片和視頻,以及從相機拍攝視頻和照片。
從相冊選擇
HarmonyOS 提供了 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<string> | 否 | 預選擇圖片的uri數據。 元服務API:?從API version 11開始,該接口支持在元服務中使用。 |
isPreviewForSingleSelectionSupported12+ | boolean | 否 | 單選模式下是否需要進大圖預覽,true表示需要,false表示不需要,默認為true。 元服務API:?從API version 12開始,該接口支持在元服務中使用。 |
綜合上述配置說明,通過PhotoViewPicker可以實現最開始我們提到的:
- 支持選擇圖片和視頻
- 支持設置最多選擇媒體數量
- 支持是否選擇原圖
不支持選擇視頻的最大最小長度配置。
看到這如果還有不知道從哪里開始入手了解鴻蒙開發技術、想要更深的掌握鴻蒙開發技術知識點的朋友們,或者是轉行求職人員還在為面試問題而犯難的,可以動動手指進來參考一下針對?鴻蒙開發學習?而設計的系統性學習方案,涵蓋基礎入門到進階實戰項目相關學習文檔:【鴻蒙開發學習指南】https://docs.qq.com/doc/DSk9ZeU9RTUhETm53
PhotoSelectResult是返回圖庫選擇后的結果集,結構如下:
名稱 | 類型 | 可讀 | 可寫 | 說明 |
---|---|---|---|---|
photoUris | Array<string> | 是 | 是 | 返回圖庫選擇后的媒體文件的uri數組,此uri數組只能通過臨時授權的方式調用photoAccessHelper.getAssets接口去使用,具體使用方式參見用戶文件uri介紹中的媒體文件uri的使用方式。 |
isOriginalPhoto | boolean | 是 | 是 | 返回圖庫選擇后的媒體文件是否為原圖。 |
返回用戶選中的媒體列表和是否選中原圖。這里產生一個問題,如果既有選擇視頻又有選擇圖片,如何根據一個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 *4if (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寫入到目標文件,并且指定圖片格式和質量。
從相機拍攝
上面實現了從相冊獲取照片,接下來實現使用攝像機拍照和拍視頻功能。
有兩種方式可以實現打開系統相機進行拍攝。
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指定了攝像頭是前置還是后置,主要有一下屬性:
名稱 | 類型 | 必填 | 說明 |
---|---|---|---|
cameraPosition | camera.CameraPosition | 是 | 相機的位置。 |
saveUri | string | 否 | 保存配置信息的uri。 |
videoDuration | number | 否 | 錄制的最大時長。 |
PickerResult是相機選擇器的處理結果,有一下屬性:
名稱 | 類型 | 必填 | 說明 |
---|---|---|---|
resultCode | number | 是 | 處理的結果,成功返回0,失敗返回-1。 |
resultUri | string | 是 | 返回的uri地址。若saveUri為空,resultUri為公共媒體路徑。若saveUri不為空且具備寫權限,resultUri與saveUri相同。若saveUri不為空且不具備寫權限,則無法獲取到resultUri。 |
mediaType | PickerMediaType | 是 | 返回的媒體類型。 |
根據返回結果中mediaType來表示是圖片還是視頻,按照不同媒體類型處理即可。
總結
本文介紹了HarmonyOS Next中圖片視頻選擇、圖片視頻拍攝的能力,重點分析了無需權限即可實現的photoAccessHelper和cameraPicker組件。