介紹 在ArkTS中調用相機拍照和錄像,以及如何使用媒體庫介面進行媒體文件的增、刪、改、查操作。本示例用到了 許可權管理能力 相機模塊能力介面 圖片處理介面 音視頻相關媒體業務能力介面 媒體庫管理介面 設備信息能力介面 文件存儲管理能力介面 彈窗能力介面 效果預覽 使用說明 1.啟動應用,在許可權彈窗中 ...
介紹
在ArkTS中調用相機拍照和錄像,以及如何使用媒體庫介面進行媒體文件的增、刪、改、查操作。本示例用到了
- 許可權管理能力
- 相機模塊能力介面
- 圖片處理介面
- 音視頻相關媒體業務能力介面
- 媒體庫管理介面
- 設備信息能力介面
- 文件存儲管理能力介面
- 彈窗能力介面
效果預覽
使用說明
1.啟動應用,在許可權彈窗中授權後返回應用,首頁顯示當前設備的相冊信息,首頁監聽相冊變化會刷新相冊列表。
2.點擊 + 按鈕,彈出相機、錄音、文本文件三個圖標。
3.安裝相機應用[Camera]應用後,點擊相機圖標,進入相機界面,預設是拍照模式,點擊底部拍照按鈕可以拍照,拍照完成會在底部左側顯示照片預覽圖。點擊錄像切換到錄像模式,點擊底部按鈕開始錄像,點擊結束按鈕結束錄像,結束錄像後底部左側顯示視頻圖標。點擊系統Back鍵或界面頂部返回按鈕返迴首頁。
4.點擊錄音圖標進入錄音界面,點擊右側開始按鈕開始錄音,按鈕變為暫停按鈕,點擊可以暫停和繼續錄音,點擊左側結束按鈕結束錄音返迴首頁。
5.點擊文本圖標進入文本編輯界面,輸入文本內容後點擊Save按鈕,會創建並寫入文本文件,完成後返迴首頁。
6.點擊相冊進入文件列表界面,展示相冊內的文件,列表中有刪除和重命名按鈕,點擊可以刪除文件和重命名文件。
7.安裝視頻播放[VideoPlayer]應用後,點擊視頻文件可以調起視頻播放界面播放該視頻。
相關概念
媒體庫管理:媒體庫管理提供介面對公共媒體資源文件進行管理,包括文件的增、刪、改、查等。 相機:相機模塊支持相機相關基礎功能的開發,主要包括預覽、拍照、錄像等。
工程目錄
entry/src/main/ets/
|---MainAbility
| |---MainAbility.ts // 主程式入口,應用啟動時獲取相應許可權
|---pages
| |---index.ets // 首頁
| |---AlbumPage.ets // 相冊頁面
| |---CameraPage.ets // 相機頁面
| |---RecordPage.ets // 錄音頁面
| |---DocumentPage.ets // 存儲文件頁面
|---model
| |---CameraService.ts // 相機模塊(拍照錄像模式)
| |---DateTimeUtil.ts // 日期工具包
| |---MediaUtils.ts // 媒體工具模塊
| |---RecordModel.ts // 錄音模塊(底層能力實現)
| |---TimeUtils.ts // 時間工具包
|---view
| |---BasicDataSource.ets // 初始化媒體服務數組
| |---MediaItem.ets // 定義具體的某一媒體模塊頁面
| |---MediaView.ets // 媒體模塊的前置模塊(判斷是否有展示的媒體內容)
| |---RenameDialog.ets // 重命名文件模塊
| |---TitleBar.ets // 標題欄
具體實現
- 佈局原理:定義@ObjectLink 裝飾的數組變數album存放資源文件,使用list()組件中ListItem()迴圈數組展示,加號Button(),點擊後觸發 animateTo({ duration: 500, curve: Curve.Ease })控制動畫展示,[源碼參考]。
/*
* Copyright (c) 2022-2023 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import mediaLibrary from '@ohos.multimedia.mediaLibrary';
import common from '@ohos.app.ability.common';
import Want from '@ohos.app.ability.Want';
import router from '@ohos.router';
import TitleBar from '../view/TitleBar';
import MediaUtils from '../model/MediaUtils';
import { MediaView } from '../view/MediaView';
import Logger from '../model/Logger';
@Observed
export default class Album {
constructor(public albumName: string, public count: number, public mediaType?: mediaLibrary.MediaType) {
this.albumName = albumName;
this.count = count;
this.mediaType = mediaType;
}
}
@Entry
@Component
struct Index {
private mediaUtils: MediaUtils = MediaUtils.getInstance(getContext(this))
@State albums: Array<Album> = []
@State selectIndex: number = 0
@State operateVisible: boolean = false
async onPageShow() {
this.albums = [];
this.albums = await this.mediaUtils.getAlbums();
}
@Builder OperateBtn(src, zIndex, translate, handleClick) {
Button() {
Image(src)
.width('70%')
.height('70%')
}
.type(ButtonType.Circle)
.width('40%')
.height('40%')
.backgroundColor('#0D9FFB')
.zIndex(zIndex)
.translate({ x: translate.x, y: translate.y })
.transition({ type: TransitionType.Insert, translate: { x: 0, y: 0 } })
.transition({ type: TransitionType.Delete, opacity: 0 })
.onClick(handleClick)
}
build() {
Stack({ alignContent: Alignment.BottomEnd }) {
Column() {
TitleBar()
List() {
ForEach(this.albums, (item: Album, index) => {
ListItem() {
MediaView({ album: item })
.id(`mediaType${index}`)
}
}, item => item.albumName)
}
.divider({ strokeWidth: 1, color: Color.Gray, startMargin: 16, endMargin: 16 })
.layoutWeight(1)
}
.width('100%')
.height('100%')
Stack({ alignContent: Alignment.Center }) {
Button() {
Image($r('app.media.add'))
.width('100%')
.height('100%')
}
.width(60)
.height(60)
.padding(10)
.id('addBtn')
.type(ButtonType.Circle)
.backgroundColor('#0D9FFB')
.onClick(() => {
animateTo({ duration: 500, curve: Curve.Ease }, () => {
this.operateVisible = !this.operateVisible
})
})
Button() {
Image($r('app.media.icon_camera'))
.id('camera')
.width('100%')
.height('100%')
}
.width(60)
.height(60)
.padding(10)
.type(ButtonType.Circle)
.backgroundColor('#0D9FFB')
.translate({ x: 0, y: -80 })
.visibility(this.operateVisible ? Visibility.Visible : Visibility.None)
.onClick(() => {
this.operateVisible = !this.operateVisible;
let context: common.UIAbilityContext | undefined = AppStorage.Get('context');
let want: Want = {
bundleName: "com.samples.camera_page",
abilityName: "EntryAbility",
};
context && context.startAbility(want, (err) => {
if (err.code) {
Logger.error('StartAbility', `Failed to startAbility. Code: ${err.code}, message: ${err.message}`);
}
});
})
Button() {
Image($r('app.media.icon_record'))
.id('record')
.width('100%')
.height('100%')
}
.width(60)
.height(60)
.padding(10)
.type(ButtonType.Circle)
.backgroundColor('#0D9FFB')
.translate({ x: -80, y: 0 })
.visibility(this.operateVisible ? Visibility.Visible : Visibility.None)
.onClick(() => {
this.operateVisible = !this.operateVisible
router.push({ url: 'pages/RecordPage' })
})
Button() {
Image($r('app.media.icon_document'))
.width('100%')
.height('100%')
}
.width(60)
.height(60)
.padding(10)
.id('document')
.type(ButtonType.Circle)
.backgroundColor('#0D9FFB')
.translate({ x: 0, y: 80 })
.visibility(this.operateVisible ? Visibility.Visible : Visibility.None)
.onClick(() => {
this.operateVisible = !this.operateVisible
router.pushUrl({ url: 'pages/DocumentPage' })
})
}
.width(180)
.height(220)
.margin({ right: 40, bottom: 120 })
}
.width('100%')
.height('100%')
}
aboutToDisappear() {
this.mediaUtils.offDateChange()
}
}
- 獲取資源文件:通過引入媒體庫實例(入口)介面@ohos.multimedia.medialibrary,例如通過this.getFileAssetsFromType(mediaLibrary.MediaType.FILE)獲取FILE類型的文件資源,並通過albums.push()添加至album數組中。
- 展示系統資源文件:當album內的值被修改時,只會讓用 @ObjectLink 裝飾的變數album所在的組件被刷新,當前組件不會刷新。
- 錄音功能:通過引入音視頻介面@ohos.multimedia.media,例如通過media.createAudioRecorder()創建音頻錄製的實例來控制音頻的錄製,通過this.audioRecorder.on('prepare', () => {this.audioRecorder.start()})非同步方式開始音頻錄製,[源碼參考]
/*
* Copyright (c) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import media from '@ohos.multimedia.media'
import Logger from '../model/Logger'
let audioConfig = {
audioSourceType: 1,
audioEncoder: 3,
audioEncodeBitRate: 22050,
audioSampleRate: 22050,
numberOfChannels: 2,
format: 6,
uri: ''
}
export default class RecordModel {
private tag: string = 'RecordModel'
private audioRecorder: media.AudioRecorder = undefined
initAudioRecorder(handleStateChange: () => void) {
this.release();
this.audioRecorder = media.createAudioRecorder()
Logger.info(this.tag, 'create audioRecorder success')
this.audioRecorder.on('prepare', () => {
Logger.info(this.tag, 'setCallback prepare case callback is called')
this.audioRecorder.start()
})
this.audioRecorder.on('start', () => {
Logger.info(this.tag, 'setCallback start case callback is called')
handleStateChange()
})
this.audioRecorder.on('stop', () => {
Logger.info(this.tag, 'audioRecorder stop called')
this.audioRecorder.release()
})
this.audioRecorder.on('pause', () => {
Logger.info(this.tag, 'audioRecorder pause finish')
handleStateChange()
})
this.audioRecorder.on('resume', () => {
Logger.info(this.tag, 'audioRecorder resume finish')
handleStateChange()
})
}
release() {
if (typeof (this.audioRecorder) !== `undefined`) {
Logger.info(this.tag, 'audioRecorder release')
this.audioRecorder.release()
this.audioRecorder = undefined
}
}
startRecorder(pathName: string) {
Logger.info(this.tag, `startRecorder, pathName = ${pathName}`)
if (typeof (this.audioRecorder) !== 'undefined') {
Logger.info(this.tag, 'start prepare')
audioConfig.uri = pathName
this.audioRecorder.prepare(audioConfig)
} else {
Logger.error(this.tag, 'case failed, audioRecorder is null')
}
}
pause() {
Logger.info(this.tag, 'audioRecorder pause called')
if (typeof (this.audioRecorder) !== `undefined`) {
this.audioRecorder.pause()
}
}
resume() {
Logger.info(this.tag, 'audioRecorder resume called')
if (typeof (this.audioRecorder) !== `undefined`) {
this.audioRecorder.resume()
}
}
finish() {
if (typeof (this.audioRecorder) !== `undefined`) {
this.audioRecorder.stop()
}
}
}
- 拍照錄像功能:通過引入相機模塊介面@ohos.multimedia.camera,例如通過this.cameraManager.createCaptureSession()創建相機入口的實例來控制拍照和錄像,通過this.captureSession.start()開始會話工作,[源碼參考]
/*
* Copyright (c) 2022 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import camera from '@ohos.multimedia.camera'
import deviceInfo from '@ohos.deviceInfo'
import fileio from '@ohos.fileio'
import image from '@ohos.multimedia.image'
import media from '@ohos.multimedia.media'
import mediaLibrary from '@ohos.multimedia.mediaLibrary'
import Logger from '../model/Logger'
import MediaUtils from '../model/MediaUtils'
const CameraMode = {
MODE_PHOTO: 0, // 拍照模式
MODE_VIDEO: 1 // 錄像模式
}
const CameraSize = {
WIDTH: 1920,
HEIGHT: 1080
}
export default class CameraService {
private tag: string = 'CameraService'
private context: any = undefined
private mediaUtil: MediaUtils = undefined
private cameraManager: camera.CameraManager = undefined
private cameras: Array<camera.CameraDevice> = undefined
private cameraId: string = ''
private cameraInput: camera.CameraInput = undefined
private previewOutput: camera.PreviewOutput = undefined
private photoOutPut: camera.PhotoOutput = undefined
private captureSession: camera.CaptureSession = undefined
private mReceiver: image.ImageReceiver = undefined
private photoUri: string = ''
private fileAsset: mediaLibrary.FileAsset = undefined
private fd: number = -1
private curMode = CameraMode.MODE_PHOTO
private videoRecorder: media.VideoRecorder = undefined
private videoOutput: camera.VideoOutput = undefined
private handleTakePicture: (photoUri: string) => void = undefined
private cameraOutputCapability: camera.CameraOutputCapability = undefined
private videoConfig: any = {
audioSourceType: 1,
videoSourceType: 0,
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: '',
orientationHint: 0,
location: {
latitude: 30, longitude: 130
},
maxSize: 10000,
maxDuration: 10000
}
constructor(context: any) {
this.context = context
this.mediaUtil = MediaUtils.getInstance(context)
this.mReceiver = image.createImageReceiver(CameraSize.WIDTH, CameraSize.HEIGHT, 4, 8)
Logger.info(this.tag, 'createImageReceiver')
this.mReceiver.on('imageArrival', () => {
Logger.info(this.tag, 'imageArrival')
this.mReceiver.readNextImage((err, image) => {
Logger.info(this.tag, 'readNextImage')
if (err || image === undefined) {
Logger.error(this.tag, 'failed to get valid image')
return
}
image.getComponent(4, (errMsg, img) => {
Logger.info(this.tag, 'getComponent')
if (errMsg || img === undefined) {
Logger.info(this.tag, 'failed to get valid buffer')
return
}
let buffer = new ArrayBuffer(4096)
if (img.byteBuffer) {
buffer = img.byteBuffer
} else {
Logger.error(this.tag, 'img.byteBuffer is undefined')
}
this.savePicture(buffer, image)
})
})
})
}
async savePicture(buffer: ArrayBuffer, img: image.Image) {
Logger.info(this.tag, 'savePicture')
this.fileAsset = await this.mediaUtil.createAndGetUri(mediaLibrary.MediaType.IMAGE)
this.photoUri = this.fileAsset.uri
Logger.info(this.tag, `this.photoUri = ${this.photoUri}`)
this.fd = await this.mediaUtil.getFdPath(this.fileAsset)
Logger.info(this.tag, `this.fd = ${this.fd}`)
await fileio.write(this.fd, buffer)
await this.fileAsset.close(this.fd)
await img.release()
Logger.info(this.tag, 'save image done')
if (this.handleTakePicture) {
this.handleTakePicture(this.photoUri)
}
}
async initCamera(surfaceId: string) {
Logger.info(this.tag, 'initCamera')
await this.releaseCamera()
Logger.info(this.tag, `deviceInfo.deviceType = ${deviceInfo.deviceType}`)
if (deviceInfo.deviceType === 'default') {
this.videoConfig.videoSourceType = 1
} else {
this.videoConfig.videoSourceType = 0
}
this.cameraManager = await camera.getCameraManager(this.context)
Logger.info(this.tag, 'getCameraManager')
this.cameras = await this.cameraManager.getSupportedCameras()
Logger.info(this.tag, `get cameras ${this.cameras.length}`)
if (this.cameras.length === 0) {
Logger.info(this.tag, 'cannot get cameras')
return
}
let cameraDevice = this.cameras[0]
this.cameraInput = await this.cameraManager.createCameraInput(cameraDevice)
this.cameraInput.open()
Logger.info(this.tag, 'createCameraInput')
this.cameraOutputCapability = await this.cameraManager.getSupportedOutputCapability(cameraDevice)
let previewProfile = this.cameraOutputCapability.previewProfiles[0]
this.previewOutput = await this.cameraManager.createPreviewOutput(previewProfile, surfaceId)
Logger.info(this.tag, 'createPreviewOutput')
let mSurfaceId = await this.mReceiver.getReceivingSurfaceId()
let photoProfile = this.cameraOutputCapability.photoProfiles[0]
this.photoOutPut = await this.cameraManager.createPhotoOutput(photoProfile, mSurfaceId)
this.captureSession = await this.cameraManager.createCaptureSession()
Logger.info(this.tag, 'createCaptureSession')
await this.captureSession.beginConfig()
Logger.info(this.tag, 'beginConfig')
await this.captureSession.addInput(this.cameraInput)
await this.captureSession.addOutput(this.previewOutput)
await this.captureSession.addOutput(this.photoOutPut)
await this.captureSession.commitConfig()
await this.captureSession.start()
Logger.info(this.tag, 'captureSession start')
}
setTakePictureCallback(callback) {
this.handleTakePicture = callback
}
async takePicture() {
Logger.info(this.tag, 'takePicture')
if (this.curMode === CameraMode.MODE_VIDEO) {
this.curMode = CameraMode.MODE_PHOTO
}
let photoSettings = {
rotation: camera.ImageRotation.ROTATION_0,
quality: camera.QualityLevel.QUALITY_LEVEL_MEDIUM,
location: { // 位置信息,經緯度
latitude: 12.9698,
longitude: 77.7500,
altitude: 1000
},
mirror: false
}
await this.photoOutPut.capture(photoSettings)
Logger.info(this.tag, 'takePicture done')
AppStorage.Set('isRefresh', true)
}
async startVideo() {
Logger.info(this.tag, 'startVideo begin')
await this.captureSession.stop()
await this.captureSession.beginConfig()
if (this.curMode === CameraMode.MODE_PHOTO) {
this.curMode = CameraMode.MODE_VIDEO
if (this.photoOutPut) {
await this.captureSession.removeOutput(this.photoOutPut)
this.photoOutPut.release()
}
} else {
if (this.videoOutput) {
await this.captureSession.removeOutput(this.videoOutput)
}
}
if (this.videoOutput) {
await this.captureSession.removeOutput(this.videoOutput)
await this.videoOutput.release()
}
this.fileAsset = await this.mediaUtil.createAndGetUri(mediaLibrary.MediaType.VIDEO)
this.fd = await this.mediaUtil.getFdPath(this.fileAsset)
this.videoRecorder = await media.createVideoRecorder()
this.videoConfig.url = `fd://${this.fd}`
await this.videoRecorder.prepare(this.videoConfig)
let videoId = await this.videoRecorder.getInputSurface()
let videoProfile = this.cameraOutputCapability.videoProfiles[0];
this.videoOutput = await this.cameraManager.createVideoOutput(videoProfile, videoId)
await this.captureSession.addOutput(this.videoOutput)
await this.captureSession.commitConfig()
await this.captureSession.start()
await this.videoOutput.start()
await this.videoRecorder.start()
Logger.info(this.tag, 'startVideo end')
}
async stopVideo() {
Logger.info(this.tag, 'stopVideo called')
await this.videoRecorder.stop()
await this.videoOutput.stop()
await this.videoRecorder.release()
await this.fileAsset.close(this.fd)
}
async releaseCamera() {
Logger.info(this.tag, 'releaseCamera')
if (this.cameraInput) {
await this.cameraInput.close()
}
if (this.previewOutput) {
await this.previewOutput.release()
}
if (this.photoOutPut) {
await this.photoOutPut.release()
}
if (this.videoOutput) {
await this.videoOutput.release()
}
if (this.captureSession) {
await this.captureSession.release()
}
}
}
鴻蒙NEXT開發知識已更新在gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md
可參考學習更多。
約束與限制
1.rk3568底層錄像功能有問題,暫不支持錄像功能,當前拍照功能僅支持部分機型。
2.本示例僅支持標準系統上運行。
3.本示例為Stage模型,已適配API version 9版本SDK,版本號:3.2.11.9;
4.本示例需要使用DevEco Studio 3.1 Beta2 (Build Version: 3.1.0.400, built on April 7, 2023)及以上版本才可編譯運行。