介紹
在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)及以上版本才可編譯執行。