文档管理中心
FAQ媒体开发拍照和图片相机开发(Camera)自定义相机拍照如何自定义快门声音

自定义相机拍照如何自定义快门声音

问题现象

在自定义相机中,自定义快门声音可以让用户根据个人喜好选择不同的快门声音,获得更加个性化的体验,如何自定义快门声音?

背景知识

  • 自定义相机拍照前需要创建拍照输出流、进行拍照设置(如图片质量、旋转角度、是否镜像等),然后再调用capture方法将拍照设置传递到拍照输出流中。
  • 应用开发时,经常需要使用一些急促简短的音效(如相机快门音效、系统通知音效等),此时可以使用SoundPool实现一次加载、多次低时延播放,详情可以参考使用SoundPool播放短音频

解决方案

  1. 定义SoundPoolUtil类,使用SoundPool完成快门音效的播放。
    收起
    自动换行
    深色代码主题
    复制
    1. import { media } from '@kit.MediaKit';
    2. import { audio } from '@kit.AudioKit';
    3. import { BusinessError } from '@kit.BasicServicesKit';
    4. import { hilog } from '@kit.PerformanceAnalysisKit';
    5. const DOMAIN: number = 0x0000;
    6. const TAG: string = '[SoundPoolPage]';
    7. export class SoundPoolUtil {
    8. private soundPool: media.SoundPool | undefined = undefined;
    9. private soundId: number = 0;
    10. private streamId: number = 0;
    11. private currentContext: Context;
    12. constructor(uiContext: Context) {
    13. this.currentContext = uiContext;
    14. this.create();
    15. }
    16. async create() {
    17. try {
    18. // audioRenderInfo中的参数usage取值为STREAM_USAGE_UNKNOWNSTREAM_USAGE_MUSICSTREAM_USAGE_MOVIE
    19. // STREAM_USAGE_AUDIOBOOK时,SoundPool播放短音时为混音模式,不会打断其他音频播放
    20. let audioRendererInfo: audio.AudioRendererInfo = {
    21. usage: audio.StreamUsage.STREAM_USAGE_MUSIC, // 音频流使用类型:音乐
    22. rendererFlags: 1 // 音频渲染器标志
    23. };
    24. // 创建soundPool实例
    25. this.soundPool = await media.createSoundPool(14, audioRendererInfo);
    26. // 注册监听
    27. this.loadCallback();
    28. this.finishPlayCallback();
    29. this.setErrorCallback();
    30. // 加载音频资源,开发者需要在resources/rawfile目录下替换成快门音频资源
    31. let fileDescriptor = await this.currentContext!.resourceManager.getRawFd('shutter.mp3');
    32. hilog.info(DOMAIN, TAG, `Successfully get the fileDescriptor: ${fileDescriptor}`);
    33. this.soundId = await this.soundPool!.load(fileDescriptor.fd, fileDescriptor.offset, fileDescriptor.length);
    34. hilog.info(DOMAIN, TAG, `Successfully load soundPool soundId: ${this.soundId}`);
    35. } catch (e) {
    36. hilog.error(DOMAIN, TAG, `CreateSoundPool error: ${e}`);
    37. }
    38. }
    39. async loadCallback() {
    40. // 加载完成回调
    41. this.soundPool!.on('loadComplete', (soundId_: number) => {
    42. this.soundId = soundId_;
    43. hilog.info(DOMAIN, TAG, `Successfully loadComplete soundId: ${soundId_}`);
    44. });
    45. }
    46. // 设置播放完成监听
    47. async finishPlayCallback() {
    48. this.soundPool!.on('playFinished', () => {
    49. hilog.info(DOMAIN, TAG, 'receive play finished message');
    50. });
    51. }
    52. // 设置错误类型监听
    53. async setErrorCallback() {
    54. this.soundPool!.on('error', (error: BusinessError) => {
    55. hilog.error(DOMAIN, TAG, `error happened,message is: ${error.code}`);
    56. hilog.error(DOMAIN, TAG, `error happened,message is: ${error.message}`);
    57. });
    58. }
    59. async PlaySoundPool() {
    60. let playParameters: media.PlayParameters = {
    61. loop: 0, // 循环0次,即播放1
    62. rate: 1, // 1倍速播放
    63. leftVolume: 0.5, // 取值范围0.0-1.0
    64. rightVolume: 0.5, // 取值范围0.0-1.0
    65. priority: 0, // 最低优先级
    66. };
    67. // 开始播放,请在音频资源加载完毕,即收到loadComplete回调之后再执行play操作
    68. this.soundPool!.play(this.soundId, playParameters, (error, streamID: number) => {
    69. if (error) {
    70. hilog.error(DOMAIN, TAG, `play sound Error: errCode is ${error.code}, errMessage is ${error.message}`);
    71. } else {
    72. this.streamId = streamID;
    73. hilog.info(DOMAIN, TAG, `play success soundid: ${this.streamId}`);
    74. }
    75. });
    76. // 设置循环播放次数
    77. await this.soundPool!.setLoop(this.streamId, 1);
    78. // 设置对应流的优先级
    79. await this.soundPool!.setPriority(this.streamId, 1);
    80. // 设置音量
    81. await this.soundPool!.setVolume(this.streamId, 0.5, 0.5);
    82. }
    83. async release() {
    84. // 终止指定流的播放
    85. await this.soundPool!.stop(this.streamId);
    86. // 卸载音频资源
    87. await this.soundPool!.unload(this.soundId);
    88. // 关闭监听
    89. this.setOffCallback();
    90. // 释放SoundPool
    91. await this.soundPool!.release();
    92. }
    93. async setOffCallback() {
    94. this.soundPool!.off('loadComplete');
    95. this.soundPool!.off('playFinished');
    96. this.soundPool!.off('error');
    97. }
    98. }
  2. 定义相机管理类CameraShooter,完成相机会话配置、拍照、保存至媒体库等操作:
    收起
    自动换行
    深色代码主题
    复制
    1. import { camera } from '@kit.CameraKit';
    2. import { BusinessError } from '@kit.BasicServicesKit';
    3. import { photoAccessHelper } from '@kit.MediaLibraryKit';
    4. import { colorSpaceManager } from '@kit.ArkGraphics2D';
    5. import { image } from '@kit.ImageKit';
    6. import { hilog } from '@kit.PerformanceAnalysisKit';
    7. const DOMAIN: number = 0x0000;
    8. const TAG: string = '[CameraShooterPage]';
    9. let previewOutput: camera.PreviewOutput;
    10. let cameraInput: camera.CameraInput;
    11. let photoSession: camera.PhotoSession;
    12. let photoOutPut: camera.PhotoOutput;
    13. let currentContext: Context;
    14. // 期望的拍照图片尺寸
    15. let imageSize: image.Size = { width: 1920, height: 1440 };
    16. export async function cameraShooting(cameraPosition: number, surfaceId: string, context: Context): Promise<void> {
    17. currentContext = context;
    18. releaseCamera();
    19. try {
    20. // 获取相机管理对象
    21. let cameraManager: camera.CameraManager = camera.getCameraManager(context);
    22. let cameraArray: camera.CameraDevice[] = cameraManager.getSupportedCameras();
    23. if (cameraArray.length <= 0) {
    24. hilog.error(DOMAIN, TAG, 'No camera devices found!');
    25. return;
    26. }
    27. // 创建相机输入流
    28. cameraInput = cameraManager.createCameraInput(cameraArray[cameraPosition]);
    29. await cameraInput.open();
    30. // 获取支持的相机模式
    31. let sceneModes: camera.SceneMode[] = cameraManager.getSupportedSceneModes(cameraArray[cameraPosition]);
    32. // 获取支持的相机输出能力
    33. let cameraOutputCap: camera.CameraOutputCapability =
    34. cameraManager.getSupportedOutputCapability(cameraArray[cameraPosition], camera.SceneMode.NORMAL_PHOTO);
    35. let isSupportPhotoMode: boolean = sceneModes.indexOf(camera.SceneMode.NORMAL_PHOTO) >= 0;
    36. if (!isSupportPhotoMode) {
    37. hilog.error(DOMAIN, TAG, 'No supported photo mode found!');
    38. return;
    39. }
    40. if (!cameraOutputCap) {
    41. hilog.error(DOMAIN, TAG, 'No supported camera OutputCap found!');
    42. return;
    43. }
    44. // 获取预览流的Profile列表
    45. let previewProfilesArray: camera.Profile[] = cameraOutputCap.previewProfiles;
    46. // 创建预览输出流,surfaceId为显示组件的id
    47. let previewProfile: camera.Profile | undefined = undefined;
    48. for (let index = previewProfilesArray.length - 1; index >= 0; index--) {
    49. if (previewProfilesArray[index].size.width / previewProfilesArray[index].size.height ===
    50. imageSize.width / imageSize.height) {
    51. previewProfile = previewProfilesArray[index];
    52. break;
    53. }
    54. }
    55. previewOutput = cameraManager.createPreviewOutput(previewProfile, surfaceId);
    56. if (previewOutput === undefined) {
    57. return;
    58. }
    59. // 获取支持的拍照流profile
    60. let photoProfilesArray: camera.Profile[] = cameraOutputCap.photoProfiles.slice().reverse();
    61. let photoProfile: camera.Profile | undefined = undefined;
    62. if (previewProfile !== undefined) {
    63. // 选择与预览流分辨率宽高比一致的拍照流分辨率
    64. photoProfile = photoProfilesArray.find((profile: camera.Profile) => {
    65. return profile.size.height * previewProfile!.size.width === previewProfile!.size.height * profile.size.width;
    66. });
    67. }
    68. // 获取支持的拍照输出能力
    69. photoOutPut = cameraManager.createPhotoOutput(photoProfile);
    70. if (photoOutPut === undefined) {
    71. return;
    72. }
    73. // 保存图片
    74. setPhotoOutputCb(photoOutPut);
    75. // 创建相机会话
    76. photoSession = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession;
    77. if (photoSession === undefined) {
    78. return;
    79. }
    80. // 开始相机会话
    81. photoSession.beginConfig();
    82. photoSession.addInput(cameraInput);
    83. photoSession.addOutput(previewOutput);
    84. photoSession.addOutput(photoOutPut);
    85. photoSession.setColorSpace(colorSpaceManager.ColorSpace.DISPLAY_P3);
    86. await photoSession.commitConfig();
    87. await photoSession.start();
    88. } catch (error) {
    89. hilog.error(DOMAIN, TAG, `The cameraShooting call failed. error: ${JSON.stringify(error)}`);
    90. }
    91. }
    92. // 拍照方法
    93. export async function capture(isFront: boolean) {
    94. try {
    95. let settings: camera.PhotoCaptureSetting = {
    96. quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
    97. rotation: camera.ImageRotation.ROTATION_0,
    98. mirror: isFront
    99. };
    100. photoOutPut.capture(settings);
    101. } catch (error) {
    102. hilog.error(DOMAIN, TAG, `The capture call failed. error: ${error.code}`);
    103. }
    104. }
    105. // 释放相机相关资源
    106. export async function releaseCamera(): Promise<void> {
    107. try {
    108. if (photoSession) {
    109. photoSession.stop();
    110. }
    111. if (cameraInput) {
    112. cameraInput.close();
    113. }
    114. if (previewOutput) {
    115. previewOutput.release();
    116. }
    117. if (photoSession) {
    118. photoSession.release();
    119. }
    120. if (photoOutPut) {
    121. photoOutPut.off('photoAssetAvailable');
    122. photoOutPut.off('captureReady');
    123. photoOutPut.release();
    124. }
    125. } catch (error) {
    126. hilog.error(DOMAIN, TAG, `The releaseCamera call failed. error: ${error.code}`);
    127. }
    128. }
    129. // 拍照后将图片保存至媒体库
    130. export function setPhotoOutputCb(photoOutput: camera.PhotoOutput): void {
    131. photoOutput.on('photoAssetAvailable',
    132. async (_err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset): Promise<void> => {
    133. let accessHelper: photoAccessHelper.PhotoAccessHelper =
    134. photoAccessHelper.getPhotoAccessHelper(currentContext);
    135. let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest =
    136. new photoAccessHelper.MediaAssetChangeRequest(photoAsset);
    137. try {
    138. assetChangeRequest.saveCameraPhoto();
    139. await accessHelper.applyChanges(assetChangeRequest);
    140. AppStorage.setOrCreate('photoUri', await photoAsset.getThumbnail());
    141. } catch (error) {
    142. hilog.error(DOMAIN, TAG, `The setPhotoOutputCb call failed. error: ${error.code}`);
    143. }
    144. });
    145. }
  3. 在主页面中引入SoundPoolUtil类和CameraShooter类,并在点击拍照时调用SoundPool播放函数:
    收起
    自动换行
    深色代码主题
    复制
    1. import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
    2. import { display } from '@kit.ArkUI';
    3. import { hilog } from '@kit.PerformanceAnalysisKit';
    4. import {
    5. cameraShooting, capture,
    6. } from '../utils/CameraShooter';
    7. import { SoundPoolUtil } from '../utils/SoundPoolUtil';
    8. const DOMAIN: number = 0x0000;
    9. const TAG: string = '[XComponentPage]';
    10. let cameraPosition = 0;
    11. let surfaceId = '';
    12. let storage = new LocalStorage();
    13. @Component
    14. @Entry(storage)
    15. export struct CameraPage {
    16. mXComponentController: XComponentController = new XComponentController;
    17. permissions: Array<Permissions> = [
    18. 'ohos.permission.CAMERA',
    19. ];
    20. private isFront: boolean = false;
    21. // 全局context
    22. private context: Context = this.getUIContext().getHostContext()!;
    23. private displayWidth = display.getDefaultDisplaySync().width;
    24. private soundPoolUtil = new SoundPoolUtil(this.context);
    25. async aboutToAppear() {
    26. try {
    27. // aboutToAppear页面申请相机权限
    28. abilityAccessCtrl.createAtManager().requestPermissionsFromUser(this.context, this.permissions).then(() => {
    29. setTimeout(() => {
    30. cameraShooting(cameraPosition, surfaceId, this.context);
    31. }, 200);
    32. });
    33. } catch (error) {
    34. hilog.error(DOMAIN, TAG, `The cameraShooting failed. error: ${JSON.stringify(error)}`);
    35. }
    36. }
    37. build() {
    38. RelativeContainer() {
    39. // XComponent主视图
    40. XComponent({
    41. type: XComponentType.SURFACE,
    42. controller: this.mXComponentController
    43. })
    44. .onLoad(async () => {
    45. this.mXComponentController.setXComponentSurfaceRect({
    46. surfaceWidth: this.displayWidth,
    47. surfaceHeight: this.displayWidth * 4 / 3
    48. });
    49. surfaceId = this.mXComponentController.getXComponentSurfaceId();
    50. })
    51. Row() {
    52. Stack() {
    53. // 拍照按钮
    54. Image($r('app.media.capture'))
    55. .height(70)
    56. .onClick(() => {
    57. capture(this.isFront);
    58. // 播放自定义快门声音
    59. this.soundPoolUtil?.PlaySoundPool();
    60. })
    61. }
    62. }
    63. .id('controlPanel')
    64. .width('100%')
    65. .justifyContent(FlexAlign.SpaceAround)
    66. .alignRules({
    67. bottom: { anchor: '__container__', align: VerticalAlign.Bottom }
    68. })
    69. .margin({ bottom: '40vp' })
    70. }
    71. .height('100%')
    72. .backgroundColor(Color.Black)
    73. }
    74. }
    75. export async function fromBack(context: Context): Promise<void> {
    76. cameraShooting(cameraPosition, surfaceId, context);
    77. }
在 FAQ 中进行搜索
请输入您想要搜索的关键词