介紹
在ArkTS中調(diào)用相機(jī)拍照和錄像,以及如何使用媒體庫接口進(jìn)行媒體文件的增、刪、改、查操作。本示例用到了
- 權(quán)限管理能力
- 相機(jī)模塊能力接口
- 圖片處理接口
- 音視頻相關(guān)媒體業(yè)務(wù)能力接口
- 媒體庫管理接口
- 設(shè)備信息能力接口
- 文件存儲管理能力接口
- 彈窗能力接口
效果預(yù)覽
使用說明
1.啟動應(yīng)用,在權(quán)限彈窗中授權(quán)后返回應(yīng)用,首頁顯示當(dāng)前設(shè)備的相冊信息,首頁監(jiān)聽相冊變化會刷新相冊列表。
2.點(diǎn)擊 + 按鈕,彈出相機(jī)、錄音、文本文件三個圖標(biāo)。
3.安裝相機(jī)應(yīng)用[Camera]應(yīng)用后,點(diǎn)擊相機(jī)圖標(biāo),進(jìn)入相機(jī)界面,默認(rèn)是拍照模式,點(diǎn)擊底部拍照按鈕可以拍照,拍照完成會在底部左側(cè)顯示照片預(yù)覽圖。點(diǎn)擊錄像切換到錄像模式,點(diǎn)擊底部按鈕開始錄像,點(diǎn)擊結(jié)束按鈕結(jié)束錄像,結(jié)束錄像后底部左側(cè)顯示視頻圖標(biāo)。點(diǎn)擊系統(tǒng)Back鍵或界面頂部返回按鈕返回首頁。
4.點(diǎn)擊錄音圖標(biāo)進(jìn)入錄音界面,點(diǎn)擊右側(cè)開始按鈕開始錄音,按鈕變?yōu)闀和0粹o,點(diǎn)擊可以暫停和繼續(xù)錄音,點(diǎn)擊左側(cè)結(jié)束按鈕結(jié)束錄音返回首頁。
5.點(diǎn)擊文本圖標(biāo)進(jìn)入文本編輯界面,輸入文本內(nèi)容后點(diǎn)擊Save按鈕,會創(chuàng)建并寫入文本文件,完成后返回首頁。
6.點(diǎn)擊相冊進(jìn)入文件列表界面,展示相冊內(nèi)的文件,列表中有刪除和重命名按鈕,點(diǎn)擊可以刪除文件和重命名文件。
7.安裝視頻播放[VideoPlayer]應(yīng)用后,點(diǎn)擊視頻文件可以調(diào)起視頻播放界面播放該視頻。
相關(guān)概念
媒體庫管理:媒體庫管理提供接口對公共媒體資源文件進(jìn)行管理,包括文件的增、刪、改、查等。 相機(jī):相機(jī)模塊支持相機(jī)相關(guān)基礎(chǔ)功能的開發(fā),主要包括預(yù)覽、拍照、錄像等。
工程目錄
entry/src/main/ets/
|---MainAbility
| |---MainAbility.ts // 主程序入口,應(yīng)用啟動時獲取相應(yīng)權(quán)限
|---pages
| |---index.ets // 首頁
| |---AlbumPage.ets // 相冊頁面
| |---CameraPage.ets // 相機(jī)頁面
| |---RecordPage.ets // 錄音頁面
| |---DocumentPage.ets // 存儲文件頁面
|---model
| |---CameraService.ts // 相機(jī)模塊(拍照錄像模式)
| |---DateTimeUtil.ts // 日期工具包
| |---MediaUtils.ts // 媒體工具模塊
| |---RecordModel.ts // 錄音模塊(底層能力實(shí)現(xiàn))
| |---TimeUtils.ts // 時間工具包
|---view
| |---BasicDataSource.ets // 初始化媒體服務(wù)數(shù)組
| |---MediaItem.ets // 定義具體的某一媒體模塊頁面
| |---MediaView.ets // 媒體模塊的前置模塊(判斷是否有展示的媒體內(nèi)容)
| |---RenameDialog.ets // 重命名文件模塊
| |---TitleBar.ets // 標(biāo)題欄
具體實(shí)現(xiàn)
- 布局原理:定義@ObjectLink 裝飾的數(shù)組變量album存放資源文件,使用list()組件中ListItem()循環(huán)數(shù)組展示,加號Button(),點(diǎn)擊后觸發(fā) 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()
}
}
- 獲取資源文件:通過引入媒體庫實(shí)例(入口)接口@ohos.multimedia.medialibrary,例如通過this.getFileAssetsFromType(mediaLibrary.MediaType.FILE)獲取FILE類型的文件資源,并通過albums.push()添加至album數(shù)組中。
- 展示系統(tǒng)資源文件:當(dāng)album內(nèi)的值被修改時,只會讓用 @ObjectLink 裝飾的變量album所在的組件被刷新,當(dāng)前組件不會刷新。
- 錄音功能:通過引入音視頻接口@ohos.multimedia.media,例如通過media.createAudioRecorder()創(chuàng)建音頻錄制的實(shí)例來控制音頻的錄制,通過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()
}
}
}
- 拍照錄像功能:通過引入相機(jī)模塊接口@ohos.multimedia.camera,例如通過this.cameraManager.createCaptureSession()創(chuàng)建相機(jī)入口的實(shí)例來控制拍照和錄像,通過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: { // 位置信息,經(jīng)緯度
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()
}
}
}
約束與限制
1.rk3568底層錄像功能有問題,暫不支持錄像功能,當(dāng)前拍照功能僅支持部分機(jī)型。
2.本示例僅支持標(biāo)準(zhǔn)系統(tǒng)上運(yùn)行。
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)及以上版本才可編譯運(yùn)行。
審核編輯 黃宇
-
接口
+關(guān)注
關(guān)注
33文章
8577瀏覽量
151023 -
鴻蒙
+關(guān)注
關(guān)注
57文章
2339瀏覽量
42818
發(fā)布評論請先 登錄
相關(guān)推薦
評論