diff --git a/CameraKit/DualPreview/.gitignore b/CameraKit/DualPreview/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d2ff20141ceed86d87c0ea5d99481973005bab2b --- /dev/null +++ b/CameraKit/DualPreview/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/CameraKit/DualPreview/AppScope/app.json5 b/CameraKit/DualPreview/AppScope/app.json5 new file mode 100644 index 0000000000000000000000000000000000000000..4fb9f79d7ead7eb0e2dcd87425bcfddd0535f158 --- /dev/null +++ b/CameraKit/DualPreview/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.example.CameraKit", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:layered_image", + "label": "$string:app_name" + } +} diff --git a/CameraKit/DualPreview/AppScope/resources/base/element/string.json b/CameraKit/DualPreview/AppScope/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..dcff7a161c7e37223d7fa221466db8b975b499a8 --- /dev/null +++ b/CameraKit/DualPreview/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "CustomCamera" + } + ] +} diff --git a/CameraKit/DualPreview/AppScope/resources/base/media/background.png b/CameraKit/DualPreview/AppScope/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f Binary files /dev/null and b/CameraKit/DualPreview/AppScope/resources/base/media/background.png differ diff --git a/CameraKit/DualPreview/AppScope/resources/base/media/foreground.png b/CameraKit/DualPreview/AppScope/resources/base/media/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..97014d3e10e5ff511409c378cd4255713aecd85f Binary files /dev/null and b/CameraKit/DualPreview/AppScope/resources/base/media/foreground.png differ diff --git a/CameraKit/DualPreview/AppScope/resources/base/media/layered_image.json b/CameraKit/DualPreview/AppScope/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/CameraKit/DualPreview/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/LICENSE b/CameraKit/DualPreview/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..18795a48d6b12fcdc1aa7bac9a9cb99f83815267 --- /dev/null +++ b/CameraKit/DualPreview/LICENSE @@ -0,0 +1,78 @@ + Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved. + + 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. + +Apache License, Version 2.0 +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +1.You must give any other recipients of the Work or Derivative Works a copy of this License; and +2.You must cause any modified files to carry prominent notices stating that You changed the files; and +3.You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +4.If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/CameraKit/DualPreview/README.md b/CameraKit/DualPreview/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f81c468b8200e777ceb7288a6acf18694314ffeb --- /dev/null +++ b/CameraKit/DualPreview/README.md @@ -0,0 +1,65 @@ +## 实现自定义相机功能 + +### 介绍 + +本示例调用CameraKit组件提供的接口能力,提供双路预览功能,包含: + - 前后置双路预览 + - 设置闪光灯 + - 镜头翻转 + +### 效果预览 + +![](./screenshots/devices/camera.PNG) + +使用说明: +1. 打开应用,授权后展示预览界面。 +2. 上方从左至右按钮功能依次为:闪光灯设置、双路预览设置。 +3. 下方按钮切换前后置摄像头。 + + +### 工程目录 + +``` +├──camera/src // 相机服务模块 +│ └──main/ets +│ └──cameramanagers +│ └──CameraManager.ets // 相机管理类 +├──entry/src/ // demo 业务代码 +│ └──main/ets/ +│ ├──constants +│ │ └──CameraConstants.ets // 常量文件 +│ ├──enteryability +│ │ └──EnteryAbility.ets // 应用状态检测 +│ ├──page +│ │ └──Index.ets // 应用的入口页面,相机APP首页 +│ ├──utiles +│ │ └──PermissionManager.ets // 权限申请 +│ └──viewmodels +│ └──PreviewViewModel.ets // 预览视图模块 +└──entry/src/main/resources // 应用静态资源目录 +``` + +### 具体实现 + +1. 使用Camera Kit相关能力。 + +### 相关权限 + +- ohos.permission.CAMERA:用于相机操作 +- ohos.permission.MICROPHONE:麦克风权限,用于录像 +- ohos.permission.MEDIA_LOCATION: 用于获取地理信息 +- ohos.permission.WRITE_IMAGEVIDEO:用于写入媒体文件 +- hos.permission.READ_IMAGEVIDEO:用于读取媒体文件 +- ohos.permission.APPROXIMATELY_LOCATION:用于获取当前位置 + + +### 约束与限制 + +1.本示例仅支持标准系统上运行,支持设备:华为手机、平板。 + +2.HarmonyOS系统:HarmonyOS 6.0.0 Release及以上。 + +3.DevEco Studio版本:DevEco Studio 6.0.0 Release及以上。 + +4.HarmonyOS SDK版本:HarmonyOS 6.0.0 Release SDK及以上。 + diff --git a/CameraKit/DualPreview/build-profile.json5 b/CameraKit/DualPreview/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..fe1ad1be1ce190d2dcb3a68a32c584d6be8babb6 --- /dev/null +++ b/CameraKit/DualPreview/build-profile.json5 @@ -0,0 +1,64 @@ +{ + "app": { + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "C:\\Users\\s00888898\\.ohos\\config\\default_DualPreview_0aEu6yVQ_GYI3nseL90IHsXGUNJ4qF5ie7u-kPlBQOE=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001BE5803A91B9ED88AF98081DCB80B67A57A3435A25805349ADF9447E4F87F5A556D52959A9562F5B", + "profile": "C:\\Users\\s00888898\\.ohos\\config\\default_DualPreview_0aEu6yVQ_GYI3nseL90IHsXGUNJ4qF5ie7u-kPlBQOE=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "C:\\Users\\s00888898\\.ohos\\config\\default_DualPreview_0aEu6yVQ_GYI3nseL90IHsXGUNJ4qF5ie7u-kPlBQOE=.p12", + "storePassword": "0000001B04FB919820EA7FF37CC503DA1624E2A79108E943BFD833C6A3D914D5DBC9A3ECDA4B0A0622427A" + } + } + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "compatibleSdkVersion": "6.0.0(20)", + "runtimeOS": "HarmonyOS", + "targetSdkVersion": "6.0.0(20)", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + }, + { + "name": "camera", + "srcPath": "./camera" + }, + { + "name": "commons", + "srcPath": "./commons", + } + ] +} \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/.gitignore b/CameraKit/DualPreview/camera/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/CameraKit/DualPreview/camera/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/BuildProfile.ets b/CameraKit/DualPreview/camera/BuildProfile.ets new file mode 100644 index 0000000000000000000000000000000000000000..3a501e5ddee8ea6d28961648fc7dd314a5304bd4 --- /dev/null +++ b/CameraKit/DualPreview/camera/BuildProfile.ets @@ -0,0 +1,17 @@ +/** + * Use these variables when you tailor your ArkTS code. They must be of the const type. + */ +export const HAR_VERSION = '1.0.0'; +export const BUILD_MODE_NAME = 'debug'; +export const DEBUG = true; +export const TARGET_NAME = 'default'; + +/** + * BuildProfile Class is used only for compatibility purposes. + */ +export default class BuildProfile { + static readonly HAR_VERSION = HAR_VERSION; + static readonly BUILD_MODE_NAME = BUILD_MODE_NAME; + static readonly DEBUG = DEBUG; + static readonly TARGET_NAME = TARGET_NAME; +} \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/Index.ets b/CameraKit/DualPreview/camera/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..ccfbb991d25157b984dd7911b8e1bcb8dffb967b --- /dev/null +++ b/CameraKit/DualPreview/camera/Index.ets @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 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. + */ + +export { CameraManager } from './src/main/ets/cameramanagers/CameraManager'; +export { PreviewManager } from './src/main/ets/cameramanagers/PreviewManager'; +export { PhotoManager } from './src/main/ets/cameramanagers/PhotoManager'; +export { VideoManager, AVRecorderState } from './src/main/ets/cameramanagers/VideoManager'; +export { ImageReceiverManager } from './src/main/ets/cameramanagers/ImageReceiverManager'; +export { GridLine } from './src/main/ets/components/GridLine'; +export { LevelIndicator } from './src/main/ets/components/LevelIndicator'; diff --git a/CameraKit/DualPreview/camera/build-profile.json5 b/CameraKit/DualPreview/camera/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..b7cdea1eacdfeed02f087f59a735d470c6752d09 --- /dev/null +++ b/CameraKit/DualPreview/camera/build-profile.json5 @@ -0,0 +1,21 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + + }, + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest" + } + ] +} diff --git a/CameraKit/DualPreview/camera/hvigorfile.ts b/CameraKit/DualPreview/camera/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..805c5d7f6809c51cff0b4adcc1142979f8f864b6 --- /dev/null +++ b/CameraKit/DualPreview/camera/hvigorfile.ts @@ -0,0 +1,6 @@ +import { harTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/oh-package-lock.json5 b/CameraKit/DualPreview/camera/oh-package-lock.json5 new file mode 100644 index 0000000000000000000000000000000000000000..ebb8b315c3eb0c6696e7e8e12bd9aaab4c3dd2d2 --- /dev/null +++ b/CameraKit/DualPreview/camera/oh-package-lock.json5 @@ -0,0 +1,19 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "commons@../commons": "commons@../commons" + }, + "packages": { + "commons@../commons": { + "name": "commons", + "version": "1.0.0", + "resolved": "../commons", + "registryType": "local" + } + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/oh-package.json5 b/CameraKit/DualPreview/camera/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..38b9469f09b3f247b681945414a4278dbdb95b9e --- /dev/null +++ b/CameraKit/DualPreview/camera/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "name": "camera", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "Index.ets", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "commons": "file:../commons" + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/CameraManager.ets b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/CameraManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..069e8e88526694d02957e94f2ecbdd943405147e --- /dev/null +++ b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/CameraManager.ets @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2025 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 '@kit.CameraKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { Logger } from 'commons'; +import OutputManager, { CreateOutputConfig } from './OutputManager'; + +const TAG = 'CameraManager'; + +export class CameraManager { + private cameraManager: camera.CameraManager; + session?: camera.PhotoSession | camera.VideoSession; + private cameraInput?: camera.CameraInput; + private outputManagers: OutputManager[] = []; + + constructor(context: Context, outputManagers: OutputManager[]) { + // [Start cameraManager] + this.cameraManager = camera.getCameraManager(context); + // [End cameraManager] + this.outputManagers = outputManagers; + this.addCameraStatusListener(); + } + + addCameraStatusListener() { + this.cameraManager.on('cameraStatus', (err: BusinessError, statusInfo: camera.CameraStatusInfo) => { + if (err && err.message) { + Logger.error(TAG, 'cameraStatus with errorMessage = ' + err.message); + return; + } + Logger.info(TAG, `cameraStatusInfo: camera is ${statusInfo.camera.cameraId}, status is ${statusInfo.status}`); + }); + } + + getCameraManager() { + return this.cameraManager; + } + + async start( + xComponentSurfaceId: string, + cameraPosition: camera.CameraPosition, + sceneMode: camera.SceneMode, + getProfile: (cameraOrientation: number, picFormat: camera.CameraFormat) => camera.Profile + ) { + try { + const device = this.getCameraDevice(cameraPosition); + if (!device) { + return; + } + // [Start cameraInput] + this.cameraInput = this.cameraManager.createCameraInput(device); + await this.cameraInput.open(); + // [End cameraInput] + // [Start session] + const session = this.cameraManager.createSession(sceneMode); + session.beginConfig(); + session.addInput(this.cameraInput); + // [StartExclude session] + const config: CreateOutputConfig = { + cameraManager: this.cameraManager, + device, + sceneMode, + profile: getProfile(device.cameraOrientation, camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP), + surfaceId: xComponentSurfaceId + }; + // [EndExclude session] + for (const outputManager of this.outputManagers) { + if (outputManager.isActive) { + const output = await outputManager.createOutput(config); + session.addOutput(output); + } + }; + await session.commitConfig(); + await session.start(); + // [End session] + this.session = session as (camera.PhotoSession | camera.VideoSession); + this.setFocusMode(camera.FocusMode.FOCUS_MODE_AUTO); + this.setExposureMode(camera.ExposureMode.EXPOSURE_MODE_AUTO); + } catch (e) { + Logger.error(TAG, `Failed to start camera session. Cause ${JSON.stringify(e)}`); + } + } + + async refreshOutput(oldOutput: camera.CameraOutput, newOutput: camera.CameraOutput) { + await this.session?.stop(); + this.session?.beginConfig(); + this.session?.removeOutput(oldOutput); + this.session?.addOutput(newOutput); + await this.session?.commitConfig(); + await this.session?.start(); + } + + // [Start release] + async release() { + await this.session?.stop(); + for (const outputManager of this.outputManagers) { + if (outputManager.isActive) { + await outputManager.release(); + } + }; + await this.cameraInput?.close(); + await this.session?.release(); + } + // [End release] + + // [Start getCameraDevice] + getCameraDevice(cameraPosition: camera.CameraPosition) { + const cameraDevices = this.cameraManager.getSupportedCameras(); + const device = cameraDevices?.find(device => device.cameraPosition === cameraPosition) || cameraDevices[0]; + if (!device) { + Logger.error(TAG, `Failed to get camera device. cameraPosition: ${cameraPosition}}`); + } + return device; + } + // [End getCameraDevice] + + // [Start getZoomRange] + getZoomRange() { + return this.session!.getZoomRatioRange(); + } + // [End getZoomRange] + + // [Start setFocusMode] + setFocusMode(focusMode: camera.FocusMode) { + try { + const isSupported = this.session?.isFocusModeSupported(focusMode); + if (!isSupported) { + Logger.error(TAG, `setFocusMode error: focus mode ${focusMode} is not supported`); + return; + } + this.session?.setFocusMode(focusMode); + } catch(e) { + Logger.error(TAG, 'setFocusMode error ' + JSON.stringify(e)); + } + } + // [End setFocusMode] + + // [Start setFocusPoint] + setFocusPoint(point: camera.Point) { + try { + this.session?.setFocusPoint(point); + } catch(e) { + Logger.error(TAG, 'setFocusPoint error ' + JSON.stringify(e)); + } + } + // [End setFocusPoint] + + // [Start setExposureMode] + setExposureMode(exposureMode: camera.ExposureMode) { + try { + const isSupported = this.session?.isExposureModeSupported(exposureMode); + if (!isSupported) { + Logger.error(TAG, `setExposureMode error: focus mode ${exposureMode} is not supported`); + return; + } + this.session?.setExposureMode(exposureMode); + } catch(e) { + Logger.error(TAG, 'setExposureMode error ' + JSON.stringify(e)); + } + } + // [End setExposureMode] + + // [Start setMeteringPoint] + setMeteringPoint(point: camera.Point) { + try { + this.session?.setMeteringPoint(point); + } catch(e) { + Logger.error(TAG, 'setMeteringPoint error ' + JSON.stringify(e)); + } + } + // [End setMeteringPoint] + + setZoomRatio(zoom: number) { + try { + this.session?.setZoomRatio(zoom); + } catch(e) { + Logger.error(TAG, 'setZoomRatio error ' + JSON.stringify(e)); + } + } + + // [Start setSmoothZoom] + setSmoothZoom(zoom: number) { + try { + this.session?.setSmoothZoom(zoom); + } catch(e) { + Logger.error(TAG, 'setSmoothZoom error ' + JSON.stringify(e)); + } + } + // [End setSmoothZoom] + + // [Start setFlashMode] + setFlashMode(flashMode: camera.FlashMode) { + try { + const isSupported = this.session?.isFlashModeSupported(flashMode); + if (!isSupported) { + Logger.error(TAG, `setFlashMode error: flash mode ${flashMode} is not supported`); + return; + } + this.session?.setFlashMode(flashMode); + } catch(e) { + Logger.error(TAG, 'setFlashMode error ' + JSON.stringify(e)); + } + } + // [End setFlashMode] + + setVideoStabilizationMode(stabilizationMode: camera.VideoStabilizationMode) { + try { + const session = this.session as camera.VideoSession; + const isSupported: boolean = session.isVideoStabilizationModeSupported(stabilizationMode); + if (isSupported) { + session.setVideoStabilizationMode(stabilizationMode); + } else { + Logger.error(TAG, 'stabilizationMode is not supported: ' + JSON.stringify(stabilizationMode)); + } + } catch (e) { + Logger.error(TAG, 'setVideoStabilizationMode error ' + JSON.stringify(e)); + } + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/ImageReceiverManager.ets b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/ImageReceiverManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..83b5a553745cdf4cf164dd0493197b6fbaaf3a48 --- /dev/null +++ b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/ImageReceiverManager.ets @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2025 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 { image } from '@kit.ImageKit'; +import { camera } from '@kit.CameraKit'; +import { display } from '@kit.ArkUI'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { Logger } from 'commons'; +import OutputManager, { CreateOutputConfig } from './OutputManager'; +import CameraConstant from '../constants/CameraConstants'; + +const TAG = 'ImageReceiverManager'; + +export class ImageReceiverManager implements OutputManager { + output?: camera.PreviewOutput; + isActive: boolean = true; + callback: (px: PixelMap) => void; + private position: camera.CameraPosition = camera.CameraPosition.CAMERA_POSITION_BACK; + + constructor(cb: (px: PixelMap) => void) { + this.callback = cb; + } + + async createOutput(config: CreateOutputConfig) { + const cameraOutputCap = config.cameraManager.getSupportedOutputCapability(config.device, config.sceneMode); + const displayRatio = config.profile.size.width / config.profile.size.height; + const profileWidth = config.profile.size.width; + const previewProfile = cameraOutputCap.previewProfiles + .sort((a, b) => Math.abs(a.size.width - profileWidth) - Math.abs(b.size.width - profileWidth)) + .find(pf => { + const pfDisplayRatio = pf.size.width / pf.size.height; + return pf.format === config.profile.format + && Math.abs(pfDisplayRatio - displayRatio) <= CameraConstant.PROFILE_DIFFERENCE; + }); + if (!previewProfile) { + Logger.error(TAG, 'Failed to get preview profile'); + return; + } + const surfaceId = await this.init(config.profile.size); + this.output = config.cameraManager.createPreviewOutput(previewProfile, surfaceId); + this.position = config.device.cameraPosition; + return this.output; + } + + async release() { + await this.output?.release(); + this.output = undefined; + } + + // [Start init] + async init(size: Size, format = image.ImageFormat.JPEG, capacity = 8) { + const receiver = image.createImageReceiver(size, format, capacity); + const surfaceId = await receiver.getReceivingSurfaceId(); + this.onImageArrival(receiver); + return surfaceId; + } + // [End init] + + // [Start getPixelMap] + async getPixelMap(imgComponent: image.Component, width: number, height: number, stride: number) { + if (stride === width) { + return await image.createPixelMap(imgComponent.byteBuffer, { + size: { height: height, width: width }, + srcPixelFormat: image.PixelMapFormat.NV21, + }); + } + const dstBufferSize = width * height * 1.5; + const dstArr = new Uint8Array(dstBufferSize); + for (let j = 0; j < height * 1.5; j++) { + const srcBuf = new Uint8Array(imgComponent.byteBuffer, j * stride, width); + dstArr.set(srcBuf, j * width); + } + return await image.createPixelMap(dstArr.buffer, { + size: { height: height, width: width }, + srcPixelFormat: image.PixelMapFormat.NV21, + }); + } + // [End getPixelMap] + + // [Start onImageArrival] + onImageArrival(receiver: image.ImageReceiver): void { + receiver.on('imageArrival', () => { + Logger.info(TAG, 'image arrival'); + receiver.readNextImage((err: BusinessError, nextImage: image.Image) => { + if (err || nextImage === undefined) { + Logger.error(TAG, 'readNextImage failed'); + return; + } + // [Start release] + // [Start nextImage] + nextImage.getComponent(image.ComponentType.JPEG, async (err: BusinessError, imgComponent: image.Component) => { + // [StartExclude release] + if (err || imgComponent === undefined) { + Logger.error(TAG, 'getComponent failed'); + } + if (imgComponent.byteBuffer) { + // [StartExclude onImageArrival] + const width = nextImage.size.width; + const height = nextImage.size.height; + const stride = imgComponent.rowStride; + Logger.info(TAG, `getComponent with width:${width} height:${height} stride:${stride}`); + const pixelMap = await this.getPixelMap(imgComponent, width, height, stride); + const displayRotation = display.getDefaultDisplaySync().rotation * camera.ImageRotation.ROTATION_90; + const rotation = this.output!.getPreviewRotation(displayRotation); + if (this.position === camera.CameraPosition.CAMERA_POSITION_FRONT) { + if (displayRotation === 90 || displayRotation === 270) { + await pixelMap.rotate((rotation + 180) % 360); + } else { + await pixelMap.rotate(rotation); + } + await pixelMap.flip(true, false); + } else { + await pixelMap.rotate(rotation); + } + this.callback(pixelMap); + // [EndExclude onImageArrival] + } else { + Logger.error(TAG, 'byteBuffer is null'); + } + // [EndExclude release] + // [StartExclude nextImage] + // [StartExclude onImageArrival] + nextImage.release().then(() => {Logger.info(TAG, 'image release done');}).catch((error: BusinessError) => { + Logger.error(TAG, `Release failed! Code ${error.code},message is ${error.message}`); + }); + Logger.info(TAG, 'image process done'); + // [EndExclude onImageArrival] + // [EndExclude nextImage] + }); + // [End release] + // [End nextImage] + }); + }); + } + // [End onImageArrival] +} diff --git a/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/OutputManager.ets b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/OutputManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..c476ae134b6c41de7df7b61c0cd47eb2700dfd2b --- /dev/null +++ b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/OutputManager.ets @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 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 '@kit.CameraKit'; + +export interface CreateOutputConfig { + cameraManager: camera.CameraManager; + device: camera.CameraDevice; + profile: camera.Profile; + sceneMode?: camera.SceneMode; + surfaceId?: string; +} + +// [Start OutputManager] +export default interface OutputManager { + output?: camera.CameraOutput; + isActive: boolean; + createOutput: (config: CreateOutputConfig) => Promise; + release: () => Promise; +} +// [End OutputManager] \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/PhotoManager.ets b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/PhotoManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..88401e6727752b181acee40622200ec2be45b7bd --- /dev/null +++ b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/PhotoManager.ets @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2025 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 '@kit.CameraKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { sensor } from '@kit.SensorServiceKit'; +import { Decimal } from '@kit.ArkTS'; +import { image } from '@kit.ImageKit'; +import { colorSpaceManager } from '@kit.ArkGraphics2D'; +import { geoLocationManager } from '@kit.LocationKit'; +import { Logger } from 'commons'; +import OutputManager, { CreateOutputConfig } from './OutputManager'; +import CameraConstant from '../constants/CameraConstants'; + +const TAG_LOG = 'PhotoManager'; + +export class PhotoManager implements OutputManager { + output?: camera.PhotoOutput; + isActive: boolean = true; + context: Context; + isSingle: boolean = false; + location: geoLocationManager.Location | null = null; + private callback: (pixelMap: image.PixelMap, url: string) => void = () => { + }; + + constructor(context: Context, isActive = true, isSingle: boolean) { + this.context = context; + this.isActive = isActive; + this.isSingle = isSingle; + } + + setIsActive(isActive: boolean) { + this.isActive = isActive; + } + + setCallback(callback: (pixelMap: image.PixelMap, url: string) => void) { + this.callback = callback; + } + + async createOutput(config: CreateOutputConfig) { + let cameraPhotoOutput: camera.PhotoOutput | undefined = undefined; + cameraPhotoOutput = this.createPhotoOutput(config.cameraManager, config.device, config.profile); + if (cameraPhotoOutput) { + this.output = cameraPhotoOutput; + this.setPhotoOutputCallback(this.isSingle); + } + return cameraPhotoOutput; + } + + // [Start create_photo_output] + public createPhotoOutput(cameraManager: camera.CameraManager, cameraDevice: camera.CameraDevice, + profile: camera.Profile) { + let cameraPhotoOutput: camera.PhotoOutput | undefined = undefined; + const cameraOutputCapability = + cameraManager.getSupportedOutputCapability(cameraDevice, camera.SceneMode.NORMAL_PHOTO); + let photoProfilesArray: camera.Profile[] | undefined = cameraOutputCapability?.photoProfiles; + if (photoProfilesArray?.length) { + try { + const displayRatio = profile.size.width / profile.size.height; + const profileWidth = profile.size.width; + const PhotoProfile = photoProfilesArray + .sort((a, b) => Math.abs(a.size.width - profileWidth) - Math.abs(b.size.width - profileWidth)) + .find(pf => { + const pfDisplayRatio = pf.size.width / pf.size.height; + return Math.abs(pfDisplayRatio - displayRatio) <= CameraConstant.PROFILE_DIFFERENCE && + pf.format === camera.CameraFormat.CAMERA_FORMAT_JPEG; + }); + if (!PhotoProfile) { + Logger.error(TAG_LOG, 'Failed to get photo profile'); + return; + } + cameraPhotoOutput = cameraManager.createPhotoOutput(PhotoProfile); + } catch (error) { + Logger.error(TAG_LOG, `Failed to createPhotoOutput. error: ${JSON.stringify(error)}`); + } + } + this.output = cameraPhotoOutput; + return cameraPhotoOutput; + } + + // [End create_photo_output] + + // [Start set_photo_cb_double] + async mediaLibSavePhoto(photoAsset: photoAccessHelper.PhotoAsset, + phAccessHelper: photoAccessHelper.PhotoAccessHelper): Promise { + try { + let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest = + new photoAccessHelper.MediaAssetChangeRequest(photoAsset); + assetChangeRequest.saveCameraPhoto(); + await phAccessHelper.applyChanges(assetChangeRequest); + phAccessHelper.release(); + } catch (error) { + Logger.error(TAG_LOG, `apply saveCameraPhoto failed with error: ${error.code}, ${error.message}`); + } + } + + async mediaLibRequestBuffer(photoAsset: photoAccessHelper.PhotoAsset, context: Context, + callback: (pixelMap: image.PixelMap, url: string) => void) { + class MediaDataHandler implements photoAccessHelper.MediaAssetDataHandler { + onDataPrepared(data: ArrayBuffer) { + if (data === undefined) { + Logger.error(TAG_LOG, 'Error occurred when preparing data'); + return; + } + let imageSource = image.createImageSource(data); + imageSource.createPixelMap().then((pixelMap: image.PixelMap) => { + callback(pixelMap, photoAsset.uri); + }).catch((err: BusinessError) => { + Logger.error(TAG_LOG, `createPixelMap err:${err.code}`); + }) + } + } + + let requestOptions: photoAccessHelper.RequestOptions = { + deliveryMode: photoAccessHelper.DeliveryMode.FAST_MODE, + } + const handler = new MediaDataHandler(); + await photoAccessHelper.MediaAssetManager.requestImageData(context, photoAsset, requestOptions, handler); + } + + public setPhotoOutputCbDouble(cameraPhotoOutput: camera.PhotoOutput) { + cameraPhotoOutput.on('photoAssetAvailable', + async (_err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset): Promise => { + let accessHelper: photoAccessHelper.PhotoAccessHelper = + photoAccessHelper.getPhotoAccessHelper(this.context); + this.mediaLibSavePhoto(photoAsset, accessHelper); + this.mediaLibRequestBuffer(photoAsset, this.context, this.callback); + }); + } + + // [End set_photo_cb_double] + + // [Start set_photo_cb_single] + setPhotoOutputCbSingle(photoOutput: camera.PhotoOutput, context: Context) { + photoOutput.on('photoAvailable', (errCode: BusinessError, photo: camera.Photo): void => { + if (errCode || photo === undefined) { + Logger.error(TAG_LOG, 'getPhoto failed'); + return; + } + this.mediaLibSavePhotoSingle(context, photo.main) + }); + } + // [End set_photo_cb_single] + + // [Start save_photo_single] + mediaLibSavePhotoSingle(context: Context, imageObj: image.Image) { + imageObj.getComponent(image.ComponentType.JPEG, async (errCode: BusinessError, component: image.Component) => { + if (errCode || component === undefined) { + Logger.error(TAG_LOG, 'getComponent failed'); + return; + } + const buffer: ArrayBuffer = component.byteBuffer; + if (!buffer) { + Logger.error(TAG_LOG, 'byteBuffer is null'); + return; + } + let photoType: photoAccessHelper.PhotoType = photoAccessHelper.PhotoType.IMAGE; + let extension: string = 'jpg'; + let options: photoAccessHelper.CreateOptions = { + title: 'testPhoto' + } + let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest = + photoAccessHelper.MediaAssetChangeRequest.createAssetRequest(context, photoType, extension, options); + assetChangeRequest.addResource(photoAccessHelper.ResourceType.IMAGE_RESOURCE, buffer) + assetChangeRequest.saveCameraPhoto(); + let accessHelper: photoAccessHelper.PhotoAccessHelper = + photoAccessHelper.getPhotoAccessHelper(context); + await accessHelper.applyChanges(assetChangeRequest); + let imageSource = image.createImageSource(buffer); + let pixelmap = imageSource.createPixelMapSync(); + this.callback(pixelmap, assetChangeRequest.getAsset().uri); + accessHelper.release(); + imageObj.release(); + }); + } + // [End save_photo_single] + + setPhotoOutputCallback(isSingle: boolean) { + if (!this.output) { + return; + } + if (isSingle) { + this.output?.off('photoAssetAvailable'); + this.setPhotoOutputCbSingle(this.output, this.context); + } else { + this.output?.off('photoAvailable'); + this.setPhotoOutputCbDouble(this.output); + } + } + + preparePhoto(session: camera.Session, zoomRatio?: number, flashMode?: camera.FlashMode, + focusMode?: camera.FocusMode) { + const photoSession = session as camera.PhotoSession; + this.setPhotoFlash(photoSession, flashMode); + this.setPhotoFocus(photoSession, focusMode); + this.setPhotoZoomRatio(photoSession, zoomRatio || 0); + } + + // [Start set_color_space] + setColorSpaceBeforeCommitConfig(session: camera.PhotoSession, isHdr: boolean): void { + let colorSpace: colorSpaceManager.ColorSpace = + isHdr ? colorSpaceManager.ColorSpace.DISPLAY_P3 : colorSpaceManager.ColorSpace.SRGB; + let colorSpaces: Array = []; + try { + colorSpaces = session.getSupportedColorSpaces(); + } catch (error) { + Logger.error(TAG_LOG, `The getSupportedColorSpaces call failed. error code: ${error.code}`); + } + if (!colorSpaces.includes(colorSpace)) { + Logger.info(TAG_LOG, `colorSpace: ${colorSpace} is not support`); + return; + } + Logger.info(TAG_LOG, `setColorSpace: ${colorSpace}`); + session.setColorSpace(colorSpace); + try { + let activeColorSpace: colorSpaceManager.ColorSpace = session.getActiveColorSpace(); + Logger.info(TAG_LOG, `activeColorSpace: ${activeColorSpace}`); + } catch (error) { + Logger.error(TAG_LOG, `getActiveColorSpace Faild: ${error.message}`); + } + } + // [End set_color_space] + + public checkFlash(photoSession: camera.PhotoSession) { + let flashModeStatus: boolean = false; + if (photoSession.hasFlash()) { + flashModeStatus = photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_AUTO); + } + return flashModeStatus; + } + + public setPhotoFlash(photoSession: camera.PhotoSession, flashMode?: camera.FlashMode) { + try { + if (this.checkFlash(photoSession)) { + photoSession.setFlashMode(flashMode || camera.FlashMode.FLASH_MODE_CLOSE); + } + } catch (error) { + Logger.error(TAG_LOG, `Failed to hasFlash. error: ${JSON.stringify(error)}`); + } + } + + public setPhotoFocus(photoSession: camera.PhotoSession, focusMode?: camera.FocusMode) { + const defaultMode = camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO; + try { + let focusModeStatus: boolean = photoSession.isFocusModeSupported(focusMode || defaultMode); + if (focusModeStatus) { + photoSession.setFocusMode(focusMode || defaultMode); + } + } catch (error) { + Logger.error(TAG_LOG, + `Failed to check whether the focus mode is supported. error: ${JSON.stringify(error)}`); + } + } + + public setFocusPoint(photoSession: camera.PhotoSession, focusPoint: camera.Point): void { + try { + photoSession.setFocusPoint(focusPoint); + } catch (error) { + Logger.error(TAG_LOG, `The setFocusPoint call failed. error code: ${error.code}`); + } + } + + public setPhotoZoomRatio(photoSession: camera.PhotoSession, zoomRatio?: number) { + let photoZoomRatio = 0; + if (!zoomRatio) { + try { + let zoomRatioRange: number[] = photoSession.getZoomRatioRange(); + if (zoomRatioRange?.length) { + photoZoomRatio = zoomRatioRange[0]; + } + } catch (error) { + Logger.error(TAG_LOG, `Failed to get the zoom ratio range. error: ${JSON.stringify(error)}`); + } + } + photoSession.setZoomRatio(zoomRatio || photoZoomRatio); + } + + getSupportedColorSpaces(session: camera.PhotoSession): Array { + let colorSpaces: Array = []; + try { + colorSpaces = session.getSupportedColorSpaces(); + } catch (error) { + Logger.error(TAG_LOG,`The getSupportedColorSpaces call failed. error code: ${error.code}`); + } + return colorSpaces; + } + + // [Start get_photo_rotation] + getPhotoRotation(photoOutput: camera.PhotoOutput, deviceDegree: number): camera.ImageRotation { + let photoRotation: camera.ImageRotation = camera.ImageRotation.ROTATION_0; + try { + photoRotation = photoOutput.getPhotoRotation(deviceDegree); + } catch (error) { + Logger.error(TAG_LOG, `The photoOutput.getPhotoRotation call failed. error code: ${error.code}`); + } + return photoRotation; + } + + // [End get_photo_rotation] + + // [Start capture_photo] + public async capture(isFront: boolean) { + const degree = await this.getPhotoDegree(); + const rotation = this.getPhotoRotation(this.output!, degree); + let settings: camera.PhotoCaptureSetting = { + quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, + rotation, + mirror: isFront + }; + this.output?.capture(settings, (err: BusinessError) => { + if (err) { + Logger.error(TAG_LOG, `Failed to capture the photo. error: ${JSON.stringify(err)}`); + return; + } + Logger.info(TAG_LOG, 'Callback invoked to indicate the photo capture request success.'); + }); + } + // [End capture_photo] + + private calculateDeviceDegree(x: number, y: number, z: number): number { + let deviceDegree: number = 0; + // Determine if the device is approaching a vertical position (perpendicular to the ground) + if ((x * x + y * y) * 3 < z * z) { + return deviceDegree; + } else { + // Calculate the inverse tangent value + let sd: Decimal = Decimal.atan2(y, -x) + // Convert radian values to angle values; + let sc: Decimal = Decimal.round(Number(sd) / Math.PI * 180); + // Adjust angle to be relative to vertical orientation + deviceDegree = 90 - Number(sc); + // Normalize angle to 0-360 degrees range + deviceDegree = deviceDegree >= 0 ? deviceDegree % 360 : deviceDegree % 360 + 360; + } + return deviceDegree; + } + + private getPhotoDegree() { + const promise: Promise = new Promise(resolve => { + sensor.once(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => { + let degree = this.calculateDeviceDegree(data.x, data.y, data.z); + resolve(degree); + }); + }) + return promise; + } + + // [Start is_moving_photo] + public isMovingPhotoSupported(photoOutput: camera.PhotoOutput): boolean { + let isSupported: boolean = false; + try { + isSupported = photoOutput.isMovingPhotoSupported(); + } catch (error) { + Logger.error(TAG_LOG, `The isMovingPhotoSupported call failed. error code: ${error.code}`); + } + return isSupported; + } + + // [End is_moving_photo] + + // [Start enable_moving_photo] + public enableMovingPhoto(enabled: boolean): void { + try { + this.output?.enableMovingPhoto(enabled); + } catch (error) { + Logger.error(TAG_LOG, `The enableMovingPhoto call failed. error code: ${error.code}`); + } + } + + // [End enable_moving_photo] + + // [Start photo_release] + async release() { + await this.output?.release(); + if (this.isSingle) { + this.output?.off('photoAvailable'); + } else { + this.output?.off('photoAssetAvailable'); + } + this.output = undefined; + } + // [End photo_release] +} diff --git a/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/PreviewManager.ets b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/PreviewManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..bcdb1db5c093f8b77e4dbca8b0082aac92a2bcd0 --- /dev/null +++ b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/PreviewManager.ets @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2025 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 '@kit.CameraKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { Logger } from 'commons'; +import OutputManager, { CreateOutputConfig } from './OutputManager'; +import CameraConstant from '../constants/CameraConstants'; + +const TAG_LOG = 'PreviewManager' + +export class PreviewManager implements OutputManager { + output?: camera.PreviewOutput; + isActive: boolean = true; + onPreviewStart: () => void = () => {}; + + constructor(onPreviewStart: () => void) { + this.onPreviewStart = onPreviewStart; + } + + // [Start createOutput] + async createOutput(config: CreateOutputConfig) { + const cameraOutputCap = config.cameraManager.getSupportedOutputCapability(config.device, config.sceneMode); + const displayRatio = config.profile.size.width / config.profile.size.height; + const profileWidth = config.profile.size.width; + const previewProfile = cameraOutputCap.previewProfiles + .sort((a, b) => Math.abs(a.size.width - profileWidth) - Math.abs(b.size.width - profileWidth)) + .find(pf => { + const pfDisplayRatio = pf.size.width / pf.size.height; + return pf.format === config.profile.format + && Math.abs(pfDisplayRatio - displayRatio) <= CameraConstant.PROFILE_DIFFERENCE; + }); + if (!previewProfile) { + Logger.error(TAG_LOG, 'Failed to get preview profile'); + return; + } + this.output = config.cameraManager.createPreviewOutput(previewProfile, config.surfaceId); + this.addOutputListener(this.output); + return this.output; + } + // [End createOutput] + + addOutputListener(output: camera.PreviewOutput) { + this.addFrameStartEventListener(output); + this.addFrameEndEventListener(output); + } + + // [Start onFrame] + addFrameStartEventListener(output: camera.PreviewOutput) { + output.on('frameStart', (err: BusinessError) => { + if (err !== undefined && err.code !== 0) { + Logger.error(TAG_LOG, `FrameStart callback Error, errorMessage: ${err.message}`); + return; + } + Logger.info(TAG_LOG, 'Preview frame started'); + this.onPreviewStart(); + }); + } + + addFrameEndEventListener(output: camera.PreviewOutput) { + output.on('frameEnd', (err: BusinessError) => { + if (err !== undefined && err.code !== 0) { + Logger.error(TAG_LOG, `FrameStart callback Error, errorMessage: ${err.message}`); + return; + } + Logger.info(TAG_LOG, 'Preview frame end'); + }); + } + // [End onFrame] + + // [Start release] + async release() { + await this.output?.release(); + this.output = undefined; + } + // [End release] + + // [Start getSupportedFrameRates] + getSupportedFrameRates() { + return this.output?.getSupportedFrameRates(); + } + // [End getSupportedFrameRates] + + // [Start setFrameRate] + setFrameRate(minFps: number, maxFps: number) { + try { + this.output?.setFrameRate(minFps, maxFps); + } catch(e) { + Logger.error(TAG_LOG, 'setFrameRate error ' + JSON.stringify(e)); + } + } + // [End setFrameRate] +} diff --git a/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/VideoManager.ets b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/VideoManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..c6d3794c75c8ff47210781ed7c361c29b863cc47 --- /dev/null +++ b/CameraKit/DualPreview/camera/src/main/ets/cameramanagers/VideoManager.ets @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2025 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 '@kit.MediaKit'; +import { camera } from '@kit.CameraKit'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { fileIo } from '@kit.CoreFileKit'; +import { sensor } from '@kit.SensorServiceKit'; +import { Decimal } from '@kit.ArkTS'; +import { image } from '@kit.ImageKit'; +import { colorSpaceManager } from '@kit.ArkGraphics2D'; +import { Logger } from 'commons'; +import OutputManager, { CreateOutputConfig } from './OutputManager'; +import CameraConstant from '../constants/CameraConstants'; + +const TAG_LOG = 'video'; + +enum QualityLevel { + NORMAL, + HIGHER +} + +export enum AVRecorderState { + IDLE = 'idle', + PREPARED = 'prepared', + STARTED = 'started', + PAUSED = 'paused', + STOPPED = 'stopped', + RELEASED = 'released', + ERROR = 'error' +} + +export class VideoManager implements OutputManager { + private avRecorder: media.AVRecorder | undefined = undefined; + private avConfig: media.AVRecorderConfig | undefined = undefined; + private avProfile: media.AVRecorderProfile | undefined = undefined; + private videoProfile: camera.VideoProfile | undefined = undefined; + private context: Context | undefined = undefined; + private cameraPosition: number = 0; + private qualityLevel: QualityLevel = QualityLevel.NORMAL; + output: camera.VideoOutput | undefined = undefined; + private videoUri: string = ''; + private file: fileIo.File | undefined = undefined; + state: media.AVRecorderState = AVRecorderState.IDLE; + isActive: boolean = false; + private callback: (pixelMap: image.PixelMap, url: string) => void = () => { + }; + + constructor(context: Context) { + this.context = context; + } + + setIsActive(isActive: boolean) { + this.isActive = isActive; + } + + async createOutput(config: CreateOutputConfig) { + try { + this.avRecorder = await media.createAVRecorder(); + this.avRecorder.on('stateChange', state => { + this.state = state; + Logger.info(TAG_LOG, 'on avRecorder state change: ', state) + }); + } catch (error) { + Logger.info(TAG_LOG, 'createAVRecorder call failed. error code: %{public}s', error.code); + } + if (this.avRecorder === undefined || this.avRecorder === null) { + return; + } + this.setVideoProfile(config.cameraManager, config.profile, config.device); + // this.setVideoProfile(config.cameraManager, config.profile, config.device); + await this.setAVConfig(); + await this.prepare(); + await this.createVideoOutput(config.cameraManager); + return this.output; + } + + async prepare() { + try { + if (this.avRecorder?.state === AVRecorderState.IDLE && this.avConfig) { + await this.avRecorder.prepare(this.avConfig); + Logger.info(TAG_LOG, 'Succeeded in preparing'); + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to prepare and catch error is ${error.message}`); + } + } + + isSupportMirror() { + let isSupported: boolean | undefined = this.output?.isMirrorSupported(); + return isSupported; + } + + // [Start start_video] + async start(isFront: boolean) { + try { + if (this.avRecorder?.state === AVRecorderState.PREPARED) { + if (this.isSupportMirror() && isFront) { + this.output?.enableMirror(true) + } + // [StartExclude start_video] + await this.avRecorder.updateRotation(this.getVideoRotation(await this.getGravity())); + // [EndExclude start_video] + await this.output?.start(); + await this.avRecorder?.start(); + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to start and catch error is ${error.message}`); + } + } + // [End start_video] + + // [Start stop_video] + async stop() { + try { + if (this.avRecorder?.state === AVRecorderState.STARTED + || this.avRecorder?.state === AVRecorderState.PAUSED) { + await this.avRecorder.stop(); + await this.output?.stop(); + const thumbnail = await this.getVideoThumbnail(); + if (thumbnail) { + this.callback(thumbnail, this.videoUri); + } + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to stop and catch error is ${error.message}`); + } + } + // [End stop_video] + + // [Start pause_video] + async pause() { + try { + if (this.avRecorder?.state === AVRecorderState.STARTED) { + await this.avRecorder.pause(); + await this.output?.stop(); + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to pause and catch error is ${error.message}`); + } + } + // [End pause_video] + + // [Start resume_video] + async resume() { + try { + if (this.avRecorder?.state === AVRecorderState.PAUSED) { + await this.output?.start(); + await this.avRecorder.resume(); + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to resume and catch error is ${error.message}`); + } + } + // [End resume_video] + + // [Start release_video] + async release() { + await this.avRecorder?.release(); + await this.output?.release(); + this.file && await fileIo.close(this.file.fd); + this.avRecorder?.off('stateChange'); + this.avRecorder = undefined; + this.output = undefined; + this.file = undefined; + } + // [End release_video] + + getCurrentOutput() { + return this.output; + } + + setVideoCallback(callback: (pixelMap: image.PixelMap, url: string) => void) { + this.callback = callback; + } + + // [Start create_video_output] + async createVideoOutput(cameraManager: camera.CameraManager) { + if (!this.avRecorder || this.avRecorder.state !== AVRecorderState.PREPARED) { + return; + } + try { + // [Start get_surface_id] + let videoSurfaceId = await this.avRecorder.getInputSurface(); + // [End get_surface_id] + this.output = cameraManager.createVideoOutput(this.videoProfile, videoSurfaceId); + } catch (error) { + Logger.error(TAG_LOG, + `Failed to create the output instance. error code: ${error.code}`); + } + } + + setVideoProfile(cameraManager: camera.CameraManager, targetProfile: camera.Profile, + device: camera.CameraDevice) { + this.cameraPosition = device.cameraPosition; + let cameraOutputCap: camera.CameraOutputCapability | undefined = + cameraManager.getSupportedOutputCapability(device, + camera.SceneMode.NORMAL_VIDEO); + let videoProfilesArray: camera.VideoProfile[] | undefined = cameraOutputCap?.videoProfiles; + if (videoProfilesArray?.length) { + try { + const displayRatio = targetProfile.size.width / targetProfile.size.height; + const profileWidth = targetProfile.size.width; + const videoProfile = videoProfilesArray + .sort((a, b) => Math.abs(a.size.width - profileWidth) - Math.abs(b.size.width - profileWidth)) + .find(pf => { + const pfDisplayRatio = pf.size.width / pf.size.height; + return Math.abs(pfDisplayRatio - displayRatio) <= CameraConstant.PROFILE_DIFFERENCE && + pf.format === camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP; + }); + if (!videoProfile) { + Logger.error(TAG_LOG, 'Failed to get video profile'); + return; + } + this.videoProfile = videoProfile; + } catch (error) { + Logger.error(TAG_LOG, `Failed to createPhotoOutput. error: ${JSON.stringify(error)}`); + } + } + } + // [End create_video_output] + + getCameraImageRotation(): camera.ImageRotation { + return this.cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT + ? camera.ImageRotation.ROTATION_270 + : camera.ImageRotation.ROTATION_90 + } + + async setAVConfig() { + // [Start create_file] + let options: photoAccessHelper.CreateOptions = { + title: Date.now().toString() + }; + let accessHelper: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context); + this.videoUri = await accessHelper.createAsset(photoAccessHelper.PhotoType.VIDEO, 'mp4', options); + this.file = fileIo.openSync(this.videoUri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); + // [End create_file] + + // [Start av_profile] + this.avProfile = { + audioBitrate: 48000, + audioChannels: 2, + audioCodec: media.CodecMimeType.AUDIO_AAC, + audioSampleRate: 48000, + fileFormat: media.ContainerFormatType.CFT_MPEG_4, + videoBitrate: 32000000, + videoCodec: (this.qualityLevel === QualityLevel.HIGHER && this.cameraPosition === 0) ? + media.CodecMimeType.VIDEO_HEVC : media.CodecMimeType.VIDEO_AVC, + videoFrameWidth: this.videoProfile?.size.width, + videoFrameHeight: this.videoProfile?.size.height, + videoFrameRate: this.cameraPosition === 0 ? 60 : 30, + } + // [End av_profile] + + // [Start av_config] + this.avConfig = { + audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_CAMCORDER, + videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, + profile: this.avProfile, + url: `fd://${this.file.fd}`, + metadata: { + videoOrientation: this.getCameraImageRotation().toString() + } + } + // [End av_config] + } + + getRealData(data: sensor.GravityResponse): number { + let getDeviceDegree: number = 0; + let x = data.x; + let y = data.y; + let z = data.z; + // Determine if the device is approaching a vertical position (perpendicular to the ground) + if ((x * x + y * y) * 3 < z * z) { + return getDeviceDegree; + } else { + // Calculate the inverse tangent value + let sd: Decimal = Decimal.atan2(y, -x); + // Convert radian values to angle values; + let sc: Decimal = Decimal.round(Number(sd) / 3.141592653589 * 180) + // Adjust angle to be relative to vertical orientation + getDeviceDegree = 90 - Number(sc); + // Normalize angle to 0-360 degrees range + getDeviceDegree = getDeviceDegree >= 0 ? getDeviceDegree % 360 : getDeviceDegree % 360 + 360; + } + return getDeviceDegree; + } + + async getGravity(): Promise { + try { + let isSupportedGravity: boolean = false; + let data = await sensor.getSensorList(); + for (let i = 0; i < data.length; i++) { + if (data[i].sensorId === sensor.SensorId.GRAVITY) { + isSupportedGravity = true; + break; + } + } + if (isSupportedGravity === true) { + const promise: Promise = new Promise((resolve) => { + sensor.once(sensor.SensorId.GRAVITY, (data: sensor.GravityResponse) => { + resolve(this.getRealData(data)); + }); + }) + return promise; + } else { + const promise: Promise = new Promise((resolve) => { + sensor.once(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => { + resolve(this.getRealData(data as sensor.GravityResponse)); + }); + }) + return promise; + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to getGravity and catch error is ${error.message}`); + return 0 + } + } + + // [Start get_video_rotation] + getVideoRotation(deviceDegree: number): camera.ImageRotation { + let videoRotation: camera.ImageRotation = this.getCameraImageRotation(); + try { + videoRotation = this.output!.getVideoRotation(deviceDegree); + Logger.info(TAG_LOG, `Video rotation is: ${videoRotation}`); + } catch (error) { + Logger.info(TAG_LOG, `Failed to getVideoRotation and catch error is: ${error.message}`); + } + return videoRotation; + } + // [End get_video_rotation] + + async getVideoThumbnail() { + let pixelMap: image.PixelMap | undefined = undefined; + try { + let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator(); + let dataSrc: media.AVFileDescriptor = { + fd: this.file!.fd, + }; + avImageGenerator.fdSrc = dataSrc; + let timeUs = 0; + let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC; + let param: media.PixelMapParams = { + width: 300, + height: 300 + }; + pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param); + avImageGenerator.release(); + } catch (error) { + Logger.info(TAG_LOG, `Failed to getVideoThumbnail and catch error is ${error.message}`); + } + return pixelMap; + } + + isRecording() { + return this.state === AVRecorderState.STARTED || this.state === AVRecorderState.PAUSED; + } + + + // [Start set_video_stabilization] + setVideoStabilizationMode(session: camera.VideoSession): boolean { + let mode: camera.VideoStabilizationMode = camera.VideoStabilizationMode.AUTO; + // Check whether video stabilization is supported + let isSupported: boolean = session.isVideoStabilizationModeSupported(mode); + if (!isSupported) { + Logger.info(TAG_LOG, `videoStabilizationMode: ${mode} is not support`); + return false; + } + Logger.info(TAG_LOG, `setVideoStabilizationMode: ${mode}`); + // Set video stabilization + session.setVideoStabilizationMode(mode); + let activeVideoStabilizationMode = session.getActiveVideoStabilizationMode(); + Logger.info(TAG_LOG, `activeVideoStabilizationMode: ${activeVideoStabilizationMode}`); + return isSupported; + } + // [End set_video_stabilization] + + // [Start set_video_color_space] + getSupportedColorSpaces(session: camera.VideoSession): Array { + let colorSpaces: colorSpaceManager.ColorSpace[] = []; + try { + colorSpaces = session.getSupportedColorSpaces(); + } catch (error) { + Logger.error(TAG_LOG, `The getSupportedColorSpaces call failed. error code: ${error.message}`); + } + return colorSpaces; + } + + setColorSpaceAfterCommitConfig(session: camera.VideoSession, isHdr: boolean): void { + let colorSpace: colorSpaceManager.ColorSpace = + isHdr ? colorSpaceManager.ColorSpace.BT2020_HLG_LIMIT : colorSpaceManager.ColorSpace.BT709_LIMIT; + let colorSpaces: colorSpaceManager.ColorSpace[] = this.getSupportedColorSpaces(session); + if (!colorSpaces.includes(colorSpace)) { + Logger.info(TAG_LOG, `colorSpace: ${colorSpace} is not support`); + return; + } + Logger.info(TAG_LOG, `setColorSpace: ${colorSpace}`); + session.setColorSpace(colorSpace); + try { + let activeColorSpace: colorSpaceManager.ColorSpace = session.getActiveColorSpace(); + Logger.info(TAG_LOG, `activeColorSpace: ${activeColorSpace}`); + } catch (error) { + Logger.error(TAG_LOG, `getActiveColorSpace Faild: ${error.message}`); + } + } + // [Start set_video_color_space] +} \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/src/main/ets/components/GridLine.ets b/CameraKit/DualPreview/camera/src/main/ets/components/GridLine.ets new file mode 100644 index 0000000000000000000000000000000000000000..dfdc02e402cdfe4a53766510db66ed23018d57cd --- /dev/null +++ b/CameraKit/DualPreview/camera/src/main/ets/components/GridLine.ets @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 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. + */ + +@Component +export struct GridLine { + private settings: RenderingContextSettings = new RenderingContextSettings(true); + private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); + @Prop cols: number = 3; + @Prop rows: number = 3; + @Prop strokeStyle: string |number |CanvasGradient | CanvasPattern = Color.White; + @Prop lineWidth: number = 1; + + // [Start draw] + draw() { + const ctx = this.context; + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.lineWidth; + const height = this.context.height; + const width = this.context.width; + // horizontal + for (let i = 1; i < this.cols; i++) { + const x = (width / this.cols) * i; + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + ctx.stroke(); + } + // vertical + for (let i = 1; i < this.rows; i++) { + const y = (height / this.rows) * i; + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(width, y); + ctx.stroke(); + } + } + + build() { + Canvas(this.context) + .width('100%') + .height('100%') + .hitTestBehavior(HitTestMode.Transparent) + .onReady(() => this.draw()) + } + // [End draw] +} \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/src/main/ets/components/LevelIndicator.ets b/CameraKit/DualPreview/camera/src/main/ets/components/LevelIndicator.ets new file mode 100644 index 0000000000000000000000000000000000000000..b3e6ac57d1c83d978e9842ad459dec00dfaf6791 --- /dev/null +++ b/CameraKit/DualPreview/camera/src/main/ets/components/LevelIndicator.ets @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 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 { curves, display } from '@kit.ArkUI'; +import { sensor } from '@kit.SensorServiceKit'; + +const ANGLE_DIFFERENCE: number = 3; + +// [Start LevelIndicator] +@Component +export struct LevelIndicator { + @Prop acc: sensor.AccelerometerResponse; + + getRotate() { + const rotation = display.getDefaultDisplaySync().rotation * 90; + if (rotation === 90 || rotation === 270) { + return -Math.atan2(-this.acc.y, this.acc.x) * (180 / Math.PI); + } + return -Math.atan2(-this.acc.x, this.acc.y) * (180 / Math.PI); + } + + isAlign() { + return Math.abs(this.getRotate()) - 0 <= ANGLE_DIFFERENCE + || Math.abs(Math.abs(this.getRotate()) - 90) <= ANGLE_DIFFERENCE; + } + + build() { + Stack({ alignContent: Alignment.Center }) { + Line({ + width: 200, + height: 1 + }) + // [StartExclude LevelIndicator] + .stroke(Color.White) + .endPoint([200, 0]) + // [EndExclude LevelIndicator] + .strokeDashArray([3, this.isAlign() ? 0 : 3]) + .opacity(this.isAlign() ? 1 : 0.5) + .rotate({ angle: this.getRotate(), centerX: '50%', centerY: '50%' }) + .animation({ + curve: curves.springMotion(0.6, 0.8), + iterations: 1, + playMode: PlayMode.Normal + }) + Circle() + // [StartExclude LevelIndicator] + .width(48) + .height(48) + .stroke(Color.White) + .fill(Color.Transparent) + // [EndExclude LevelIndicator] + .opacity(this.isAlign() ? 1 : 0.5) + } + // [StartExclude LevelIndicator] + .width('100%') + .height('100%') + // [EndExclude LevelIndicator] + .hitTestBehavior(HitTestMode.Transparent) + } +} +// [End LevelIndicator] diff --git a/CameraKit/DualPreview/camera/src/main/ets/constants/CameraConstants.ets b/CameraKit/DualPreview/camera/src/main/ets/constants/CameraConstants.ets new file mode 100644 index 0000000000000000000000000000000000000000..780f111cdc5b073fe0ef0f4c987da03823b69f21 --- /dev/null +++ b/CameraKit/DualPreview/camera/src/main/ets/constants/CameraConstants.ets @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2025 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. + */ + +class CameraConstant { + static readonly PROFILE_DIFFERENCE = 1e-10; +} + +export default CameraConstant; \ No newline at end of file diff --git a/CameraKit/DualPreview/camera/src/main/module.json5 b/CameraKit/DualPreview/camera/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..24f5f42e9f03181b11126c80e3876da0b72da87d --- /dev/null +++ b/CameraKit/DualPreview/camera/src/main/module.json5 @@ -0,0 +1,10 @@ +{ + "module": { + "name": "camera", + "type": "har", + "deviceTypes": [ + "phone", + "tablet" + ] + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/commons/.gitignore b/CameraKit/DualPreview/commons/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/CameraKit/DualPreview/commons/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/CameraKit/DualPreview/commons/BuildProfile.ets b/CameraKit/DualPreview/commons/BuildProfile.ets new file mode 100644 index 0000000000000000000000000000000000000000..3a501e5ddee8ea6d28961648fc7dd314a5304bd4 --- /dev/null +++ b/CameraKit/DualPreview/commons/BuildProfile.ets @@ -0,0 +1,17 @@ +/** + * Use these variables when you tailor your ArkTS code. They must be of the const type. + */ +export const HAR_VERSION = '1.0.0'; +export const BUILD_MODE_NAME = 'debug'; +export const DEBUG = true; +export const TARGET_NAME = 'default'; + +/** + * BuildProfile Class is used only for compatibility purposes. + */ +export default class BuildProfile { + static readonly HAR_VERSION = HAR_VERSION; + static readonly BUILD_MODE_NAME = BUILD_MODE_NAME; + static readonly DEBUG = DEBUG; + static readonly TARGET_NAME = TARGET_NAME; +} \ No newline at end of file diff --git a/CameraKit/DualPreview/commons/Index.ets b/CameraKit/DualPreview/commons/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..621cafbe0df5e8ab8fb2fe0240a9ecc6378d2dc5 --- /dev/null +++ b/CameraKit/DualPreview/commons/Index.ets @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025 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. + */ + +export { Logger } from './src/main/ets/utils/Logger'; diff --git a/CameraKit/DualPreview/commons/build-profile.json5 b/CameraKit/DualPreview/commons/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..284a1c3355b0643262a13c0b842eb667a433e2d3 --- /dev/null +++ b/CameraKit/DualPreview/commons/build-profile.json5 @@ -0,0 +1,20 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + }, + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest" + } + ] +} diff --git a/CameraKit/DualPreview/commons/hvigorfile.ts b/CameraKit/DualPreview/commons/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..805c5d7f6809c51cff0b4adcc1142979f8f864b6 --- /dev/null +++ b/CameraKit/DualPreview/commons/hvigorfile.ts @@ -0,0 +1,6 @@ +import { harTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/CameraKit/DualPreview/commons/oh-package.json5 b/CameraKit/DualPreview/commons/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..d50184a32b1cf153f7dc71afeb97d601086fa6a7 --- /dev/null +++ b/CameraKit/DualPreview/commons/oh-package.json5 @@ -0,0 +1,9 @@ +{ + "name": "commons", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "Index.ets", + "author": "", + "license": "Apache-2.0", + "dependencies": {} +} diff --git a/CameraKit/DualPreview/commons/src/main/ets/utils/Logger.ets b/CameraKit/DualPreview/commons/src/main/ets/utils/Logger.ets new file mode 100644 index 0000000000000000000000000000000000000000..c769a6076e62e5ec82a07d80992354b31e0b5702 --- /dev/null +++ b/CameraKit/DualPreview/commons/src/main/ets/utils/Logger.ets @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 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 { hilog } from '@kit.PerformanceAnalysisKit'; + +export class Logger { + private static readonly DOMAIN: number = 0xFF00; + private static readonly TAG: string = 'com.example.customcamera'; + private static readonly PREFIX: string = '[camera-log]'; + + public static debug(logTag: string, messageFormat: string, ...args: Object[]): void { + hilog.debug(Logger.DOMAIN, Logger.TAG, `${Logger.PREFIX} ${logTag}: ${messageFormat}`, args); + } + + public static info(logTag: string, messageFormat: string, ...args: Object[]): void { + hilog.info(Logger.DOMAIN, Logger.TAG, `${Logger.PREFIX} ${logTag}: ${messageFormat}`, args); + } + + public static warn(logTag: string, messageFormat: string, ...args: Object[]): void { + hilog.warn(Logger.DOMAIN, Logger.TAG, `${Logger.PREFIX} ${logTag}: ${messageFormat}`, args); + } + + public static error(logTag: string, messageFormat: string, ...args: Object[]): void { + hilog.error(Logger.DOMAIN, Logger.TAG, `${Logger.PREFIX} ${logTag}: ${messageFormat}`, args); + } + + public static fatal(logTag: string, messageFormat: string, ...args: Object[]): void { + hilog.fatal(Logger.DOMAIN, Logger.TAG, `${Logger.PREFIX} ${logTag}: ${messageFormat}`, args); + } + + private constructor() { + } +} diff --git a/CameraKit/DualPreview/commons/src/main/module.json5 b/CameraKit/DualPreview/commons/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..74e2fe95124ab0568a2619f91d8c8e9bdb3297c6 --- /dev/null +++ b/CameraKit/DualPreview/commons/src/main/module.json5 @@ -0,0 +1,10 @@ +{ + "module": { + "name": "commons", + "type": "har", + "deviceTypes": [ + "phone", + "tablet" + ] + } +} diff --git a/CameraKit/DualPreview/entry/.gitignore b/CameraKit/DualPreview/entry/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/CameraKit/DualPreview/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/build-profile.json5 b/CameraKit/DualPreview/entry/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..50e33db6ec3d25c19652a894127becfb3761a01b --- /dev/null +++ b/CameraKit/DualPreview/entry/build-profile.json5 @@ -0,0 +1,26 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/hvigorfile.ts b/CameraKit/DualPreview/entry/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6edcd90486dd5a853cf7d34c8647f08414ca7a3 --- /dev/null +++ b/CameraKit/DualPreview/entry/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/CameraKit/DualPreview/entry/oh-package-lock.json5 b/CameraKit/DualPreview/entry/oh-package-lock.json5 new file mode 100644 index 0000000000000000000000000000000000000000..01723a1209faf07a9d88680f47976b49b776a0e9 --- /dev/null +++ b/CameraKit/DualPreview/entry/oh-package-lock.json5 @@ -0,0 +1,29 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "camera@../camera": "camera@../camera", + "commons@../commons": "commons@../commons" + }, + "packages": { + "camera@../camera": { + "name": "camera", + "version": "1.0.0", + "resolved": "../camera", + "registryType": "local", + "dependencies": { + "commons": "file:../commons" + } + }, + "commons@../commons": { + "name": "commons", + "version": "1.0.0", + "resolved": "../commons", + "registryType": "local" + } + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/oh-package.json5 b/CameraKit/DualPreview/entry/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..4f1c83bce2a51958343ae478f42843ac49efba07 --- /dev/null +++ b/CameraKit/DualPreview/entry/oh-package.json5 @@ -0,0 +1,13 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + "commons": "file:../commons", + "camera": "file:../camera" + } +} + diff --git a/CameraKit/DualPreview/entry/src/main/ets/constants/Constants.ets b/CameraKit/DualPreview/entry/src/main/ets/constants/Constants.ets new file mode 100644 index 0000000000000000000000000000000000000000..63a319b6d0c08a541568b7e723e8fd367785b0b7 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/constants/Constants.ets @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 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 { Permissions } from "@kit.AbilityKit"; + +class CameraConstant { + static readonly RATIO_PHOTO: number = 4 / 3; + static readonly RATIO_VIDEO: number = 16 / 9; + static readonly PERMISSIONS: Permissions[] = [ + 'ohos.permission.CAMERA', + 'ohos.permission.MICROPHONE', + 'ohos.permission.MEDIA_LOCATION', + 'ohos.permission.WRITE_IMAGEVIDEO', + 'ohos.permission.READ_IMAGEVIDEO', + 'ohos.permission.APPROXIMATELY_LOCATION' + ]; +} + +export default CameraConstant; \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/ets/entryability/EntryAbility.ets b/CameraKit/DualPreview/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..fe3138ba2df7682844b5eba1433f0a721afde588 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 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 { ConfigurationConstant, UIAbility } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; +import WindowUtil from '../utils/WindowUtil'; + +const DOMAIN = 0x0000; + +export default class EntryAbility extends UIAbility { + onCreate(): void { + this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy(): void { + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + WindowUtil.enterImmersive(windowStage.getMainWindowSync()); + + windowStage.loadContent('pages/Index', (err) => { + if (err.code) { + hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); + return; + } + hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/ets/pages/Index.ets b/CameraKit/DualPreview/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..721d1b98c631d7e2054166f4da0ff076c6872931 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,534 @@ +/* + * Copyright (c) 2025 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 { sensor } from '@kit.SensorServiceKit'; +import { common } from '@kit.AbilityKit'; +import { display } from '@kit.ArkUI'; +import { curves } from '@kit.ArkUI'; +import { + CameraManager, + GridLine, + ImageReceiverManager, + LevelIndicator, + PhotoManager, + PreviewManager, + VideoManager +} from 'camera'; +import CameraConstant from '../constants/Constants'; +import { calCameraPoint, getClampedChildPosition, limitNumberInRange, showToast } from '../utils/CommonUtil'; +import RefreshableTimer from '../utils/RefreshableTimer'; +import PermissionManager from '../utils/PermissionManager'; +import ZoomButtonsView from '../views/ZoomButtonsView'; +import ModeButtonsView from '../views/ModeButtonsView'; +import SettingButtonsView from '../views/SettingButtonsView'; +import OperateButtonsView from '../views/OperateButtonsView'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; + +@Extend(SymbolGlyph) +function funcButtonStyle() { + .fontSize(22) + .fontColor([Color.White]) + .borderRadius('50%') + .padding(12) + .backgroundColor('#664D4D4D') +} + +@Entry +@Component +struct Index { + private context: Context = this.getUIContext().getHostContext()!; + private applicationContext = this.context.getApplicationContext(); + private windowClass = (this.context as common.UIAbilityContext).windowStage.getMainWindowSync(); + @State videoManager: VideoManager = new VideoManager(this.context); + @State isSinglePhoto: boolean = false; + @State isLivePhoto: boolean = false; + private photoManager: PhotoManager = new PhotoManager(this.context, true, this.isSinglePhoto); + private previewManager: PreviewManager = new PreviewManager(() => { + this.onPreviewStart() + }); + private imageReceiverManager: ImageReceiverManager = new ImageReceiverManager(px => { + this.onImageReceiver(px); + }); + private cameraManager: CameraManager = new CameraManager(this.context, [this.previewManager, + this.photoManager, this.videoManager, this.imageReceiverManager]); + @State previewVM: PreviewViewModel = new PreviewViewModel(this.getUIContext()); + @State isGridLineVisible: boolean = false; + @State isLevelIndicatorVisible: boolean = false; + @State isPreviewImageVisible: boolean = true; + @State isFocusBoxVisible: boolean = false; + @State focusBoxPosition: Edges = { top: 0, left: 0 }; + private focusBoxSize: Size = { width: 80, height: 80 }; + private focusBoxTimer: RefreshableTimer = new RefreshableTimer(() => { + this.isFocusBoxVisible = false; + }, 3 * 1000); + private exposureFontSize: number = 24; + @State isSleeping: boolean = false; + private sleepTimer?: RefreshableTimer; + private zoomRange: number[] = []; + @State zooms: number[] = [1, 5, 10]; + @State currentZoom: number = 1; + @State isZoomPinching: boolean = false; + private originZoomBeforePinch: number = 1; // record zoom after pinch, sale base it. + @State previewImage: PixelMap | ResourceStr = ''; + private PreviewImageHeight: number = 150; + @State photoDelayTime: number = 0; + @State photoRemainder: number = 0; + @State isDelayTakePhoto: boolean = false; + @State acc: sensor.AccelerometerResponse = { x: 0, y: 0, z: 0 } as sensor.AccelerometerResponse; + private setPreviewSize: () => void = () => { + this.previewVM.setPreviewSize(); + } + @State isShowBlack: boolean = false; + @StorageLink('captureClick') @Watch('onCaptureClick') captureClickFlag: number = 0; + @State flashBlackOpacity: number = 1; + + async aboutToAppear() { + this.addGravityEventListener(); + this.initSleepTimer(); + this.registerApplicationStateChange(); + this.addOrientationChangeEventListener(); + display.on('foldStatusChange', () => { + this.onFoldStatusChange() + }); + } + + aboutToDisappear(): void { + this.removeOrientationChangeEventListener(); + } + + // [Start addGravityEventListener] + addGravityEventListener() { + sensor.on(sensor.SensorId.GRAVITY, (data) => { + this.acc = data; + }, { interval: 100 * 1000 * 1000 }); // 100ms + } + + // [End addGravityEventListener] + + addOrientationChangeEventListener() { + this.windowClass.on('windowSizeChange', this.setPreviewSize); + } + + removeOrientationChangeEventListener() { + this.windowClass.off('windowSizeChange', this.setPreviewSize); + } + + onImageReceiver(pixelMap: PixelMap) { + this.previewImage = pixelMap; + } + + // [Start initSleepTimer] + initSleepTimer() { + this.sleepTimer = new RefreshableTimer(() => { + this.previewVM.openPreviewBlur(); + this.isSleeping = true; + this.cameraManager.release(); + }, 30 * 1000); + this.sleepTimer.start(); + const observer = this.getUIContext().getUIObserver(); + observer.on('willClick', () => { + this.sleepTimer?.refresh(); + }); + } + + // [End initSleepTimer] + + async onFoldStatusChange() { + await this.cameraManager.release(); + await this.startCamera(); + this.syncButtonSettings(); + } + + // [Start registerApplicationStateChange] + registerApplicationStateChange() { + this.applicationContext.on('applicationStateChange', { + onApplicationForeground: async () => { + await this.startCamera(); + // [StartExclude registerApplicationStateChange] + this.syncButtonSettings(); + // [EndExclude registerApplicationStateChange] + }, + onApplicationBackground: () => { + // [StartExclude registerApplicationStateChange] + this.previewVM.openPreviewBlur(); + // [EndExclude registerApplicationStateChange] + this.cameraManager.release(); + } + }) + } + + async startCamera() { + const cameraPosition = this.previewVM.getCameraPosition(); + const sceneMode = this.previewVM.getSceneMode(); + await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); + } + + // [End registerApplicationStateChange] + + exitApp() { + this.applicationContext.killAllProcesses(); + } + + onPreviewStart() { + this.previewVM.closePreviewBlur(); + } + + initZooms() { + const zoomRange = this.cameraManager.getZoomRange(); + const minZoom = zoomRange[0]; + this.zoomRange = zoomRange; + if (minZoom < this.zooms[0]) { + this.zooms.unshift(minZoom); + } + } + + initRates() { + const frameRates = this.previewManager.getSupportedFrameRates(); + if (frameRates && frameRates[0]) { + const minRate = frameRates[0].min; + const maxRate = frameRates[0].max; + this.previewVM.rates = [minRate, maxRate]; + this.previewVM.currentRate = maxRate; + this.previewManager.setFrameRate(maxRate, maxRate); + } + ; + } + + syncButtonSettings() { + this.previewManager.setFrameRate(this.previewVM.currentRate, this.previewVM.currentRate); + this.photoManager.enableMovingPhoto(this.isLivePhoto); + this.photoManager.setPhotoOutputCallback(this.isSinglePhoto); + } + + flashBlackAnim() { + this.flashBlackOpacity = 1; + this.isShowBlack = true; + animateToImmediately({ + curve: curves.interpolatingSpring(1, 1, 410, 38), + delay: 50, + onFinish: () => { + this.isShowBlack = false; + this.flashBlackOpacity = 1; + } + }, () => { + this.flashBlackOpacity = 0; + }) + } + + onCaptureClick(): void { + this.flashBlackAnim(); + } + + @Builder + preview() { + // [Start Stack] + Stack({ + alignContent: Alignment.Center + }) { + // [Start XComponent] + // [Start XComponent_gesture] + XComponent({ + type: XComponentType.SURFACE, + controller: this.previewVM.xComponentController + }) + .size({ height: '100%', width: '100%' }) + // [StartExclude Stack] + // [StartExclude XComponent_gesture] + .onLoad(async () => { + // [StartExclude XComponent] + await PermissionManager.request(CameraConstant.PERMISSIONS, this.context) + .catch(() => { + this.exitApp() + }); + // [EndExclude XComponent] + this.previewVM.surfaceId = this.previewVM.xComponentController.getXComponentSurfaceId(); + this.previewVM.setPreviewSize(); + this.previewVM.xComponentController.setXComponentSurfaceRotation({ lock: true }); + // [StartExclude XComponent] + await this.startCamera(); + this.initZooms(); + this.initRates(); + // [EndExclude XComponent] + }) + // [StartExclude XComponent_gesture] + // [End XComponent] + .gesture( + PinchGesture({ fingers: 2 }) + .onActionStart(() => { + this.originZoomBeforePinch = this.currentZoom; + this.isZoomPinching = true; + }) + .onActionUpdate((event: GestureEvent) => { + const targetZoom = this.originZoomBeforePinch * event.scale; + this.currentZoom = limitNumberInRange(targetZoom, this.zoomRange); + this.cameraManager.setZoomRatio(this.currentZoom); + }) + .onActionEnd(() => { + this.isZoomPinching = false; + }) + ) + // [End XComponent_gesture] + .onClick(event => { + this.isFocusBoxVisible = true; + const previewSize = this.previewVM.previewSize; + const cameraPoint = calCameraPoint( + this.getUIContext().vp2px(event.x), + this.getUIContext().vp2px(event.y), + previewSize.width, + previewSize.height + ); + this.cameraManager.setFocusPoint(cameraPoint); + this.cameraManager.setMeteringPoint(cameraPoint); + this.focusBoxPosition = getClampedChildPosition(this.focusBoxSize, { + width: this.getUIContext().px2vp(previewSize.width), + height: this.getUIContext().px2vp(previewSize.height) + }, event); + this.focusBoxTimer.refresh(); + }) + // [EndExclude Stack] + if (this.isGridLineVisible) { + GridLine() + } + // [StartExclude Stack] + if (this.isLevelIndicatorVisible) { + LevelIndicator({ + acc: this.acc + }) + } + // focus box + if (this.isFocusBoxVisible) { + Image($r('app.media.focus_box')) + .width(80) + .height(80) + .position(this.focusBoxPosition) + SymbolGlyph($r('sys.symbol.sun_max')) + .fontSize(this.exposureFontSize) + .fontColor([Color.White]) + .position(this.getExposurePosition()) + } + + if (this.isDelayTakePhoto) { + Text(`${this.photoRemainder}S`) + .fontSize(44) + .fontWeight(FontWeight.Regular) + .fontColor(Color.White) + } + // [EndExclude Stack] + + if (this.isShowBlack) { + Column() + .key('black') + .width('100%') + .height('100%') + .backgroundColor(Color.Black) + .opacity(this.flashBlackOpacity) + } + } + // .width('100%') + // .height('100%') + // [End Stack] + .alignRules({ + middle: { anchor: '__container__', align: HorizontalAlign.Center } + }) + // .width(this.previewVM.getPreviewWidth()) + // .height(this.previewVM.getPreviewHeight()) + // .margin({ top: this.previewVM.getPreviewTop() }) + // .blur(this.previewVM.blurRadius) + // .rotate(this.previewVM.blurRotation) + .size({ width: '100%', height: '100%' }) + } + + // [Start wakeupMask] + @Builder + wakeupMask() { + Column() { + Text($r('app.string.wakeup_text')) + .fontColor(Color.White) + .opacity(0.6) + } + // [StartExclude wakeupMask] + .width('100%') + .height('100%') + .backgroundColor(Color.Black) + .justifyContent(FlexAlign.Center) + // [EndExclude wakeupMask] + .onClick(async () => { + this.isSleeping = false; + this.sleepTimer?.refresh(); + await this.startCamera(); + this.syncButtonSettings(); + }) + } + + // [End wakeupMask] + + @Builder + gridLineButton() { + SymbolGlyph( + this.isGridLineVisible + ? $r('sys.symbol.camera_assistive_grid') + : $r('sys.symbol.camera_assistive_grid_slash') + ) + .funcButtonStyle() + .onClick(() => { + this.isGridLineVisible = !this.isGridLineVisible; + const message = this.isGridLineVisible ? $r('app.string.grid_line_open') : $r('app.string.grid_line_close'); + showToast(this.getUIContext(), message); + }) + } + + @Builder + levelButton() { + SymbolGlyph($r('sys.symbol.horizontal_level')) + .funcButtonStyle() + .onClick(() => { + this.isLevelIndicatorVisible = !this.isLevelIndicatorVisible; + const message = this.isLevelIndicatorVisible ? $r('app.string.level_open') : $r('app.string.level_close'); + showToast(this.getUIContext(), message); + }) + } + + @Builder + previewImageButton() { + SymbolGlyph(this.isPreviewImageVisible ? $r('sys.symbol.eye') : $r('sys.symbol.eye_slash')) + .funcButtonStyle() + .onClick(() => { + this.isPreviewImageVisible = !this.isPreviewImageVisible; + const message = + this.isPreviewImageVisible ? $r('app.string.preview_image_open') : $r('app.string.preview_image_close'); + showToast(this.getUIContext(), message); + }) + } + + getPreviewImageWidth() { + const rotation = display.getDefaultDisplaySync().rotation * 90; + const ratio = this.previewVM.getPreviewRatio(); + const displayRatio = rotation === 90 || rotation === 270 ? 1 / ratio : ratio; + return this.PreviewImageHeight / displayRatio; + } + + getExposurePosition(): Edges { + const focusBoxLeft = this.focusBoxPosition.left as number; + const focusBoxTop = this.focusBoxPosition.top as number; + const exposureWidth = this.exposureFontSize; + const exposureHeight = this.exposureFontSize; + const focusBoxWidth = this.focusBoxSize.width; + const focusBoxHeight = this.focusBoxSize.height; + const previewWidth = this.getUIContext().px2vp(this.previewVM.previewSize.width); + const GAP = 10; + const top = focusBoxTop - exposureHeight / 2 + focusBoxHeight / 2; + const left = focusBoxLeft > previewWidth / 2 + ? focusBoxLeft - GAP - exposureWidth + : focusBoxLeft + focusBoxWidth + GAP; + return { top, left }; + } + + @Builder + previewImageView() { + Image(this.previewImage) + .width(this.getPreviewImageWidth()) + .height(this.PreviewImageHeight) + .align(Alignment.TopStart) + // .alignRules({ + // top: { anchor: '__container__', align: VerticalAlign.Top }, + // left: { anchor: '__container__', align: HorizontalAlign.Start } + // }) + .margin({ + top: '40%', + left: '4%', + }) + } + + @Builder + funcButtonsView() { + Column({ space: 24 }) { + //this.gridLineButton() + //this.levelButton() + this.previewImageButton() + } + .alignRules({ + top: { anchor: 'settingButtonsView', align: VerticalAlign.Bottom }, + right: { anchor: 'settingButtonsView', align: HorizontalAlign.End } + }) + .margin({ + top: 40, + right: 10 + }) + } + + build() { + RelativeContainer() { + Stack({ alignContent: Alignment.TopStart }) { + this.preview() + if (this.isPreviewImageVisible) { + this.previewImageView() + } + } + + this.funcButtonsView() + SettingButtonsView({ + previewVM: this.previewVM, + cameraManager: this.cameraManager, + previewManager: this.previewManager, + photoManager: this.photoManager, + videoManager: this.videoManager, + photoDelayTime: this.photoDelayTime, + isSinglePhoto: this.isSinglePhoto, + isLivePhoto: this.isLivePhoto + }) + if (!this.photoRemainder) { + if (!this.previewVM.isFront) { + ZoomButtonsView({ + cameraManager: this.cameraManager, + zoomRange: this.zoomRange, + zooms: this.zooms, + currentZoom: this.currentZoom + }) + } + ModeButtonsView({ + previewVM: this.previewVM, + photoManager: this.photoManager, + videoManager: this.videoManager, + cameraManager: this.cameraManager, + ImageReceiverManager: this.imageReceiverManager, + syncButtonSettings: () => { + this.syncButtonSettings(); + } + }) + OperateButtonsView({ + previewVM: this.previewVM, + cameraManager: this.cameraManager, + photoManager: this.photoManager, + videoManager: this.videoManager, + isDelayTakePhoto: this.isDelayTakePhoto, + photoDelayTime: this.photoDelayTime, + ImageReceiverManager: this.imageReceiverManager, + photoRemainder: this.photoRemainder, + syncButtonSettings: () => { + this.syncButtonSettings(); + } + }) + } + if (this.isSleeping) { + this.wakeupMask() + } + } + .height('100%') + .width('100%') + .backgroundColor(Color.Black) + .onClick(() => { + }) + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/ets/utils/CommonUtil.ets b/CameraKit/DualPreview/entry/src/main/ets/utils/CommonUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..fcf47ba52d1008455efad159b4c1ef32803399b5 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/utils/CommonUtil.ets @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025 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 { Point } from '@kit.TestKit'; +import { camera } from '@kit.CameraKit'; +import { display } from '@kit.ArkUI'; + +export function limitNumberInRange(src: number, range: number[]) { + if (range.length < 2) return src; + if (src < range[0]) return range[0]; + if (src > range[1]) return range[1]; + return src; +} + +// find start index the target in which range +// eg: target: 1.5 arr: [0, 1, 5, 10] return 1 +export function findRangeIndex(target: number, arr: number[]) { + if (arr.length === 0) { + return -1; + } + if (target >= arr[arr.length - 1]) { + return arr.length - 1; + } + return [...arr].sort((a, b) => a - b).findIndex((n, i) => { + return target >= n && target < arr[i + 1]; + }); +} + +// Math floor float by digit +// eg: toFixed(9.97, 1) -> 9.9 +export function toFixed(num: number, digit: number): string { + const scale = 10 ** digit; + return (Math.floor(num * scale) / scale).toFixed(digit); +} + +// [Start getClampedChildPosition] +// cal absolute position in parent area +export function getClampedChildPosition(childSize: Size, parentSize: Size, point: Point): Edges { + // center point + let left = point.x - childSize.width / 2; + let top = point.y - childSize.height / 2; + // limit in left + if (left < 0) { + left = 0; + } + // limit in right + if (left + childSize.width > parentSize.width) { + left = parentSize.width - childSize.width; + } + // limit in top + if (top < 0) { + top = 0; + } + // limit in bottom + if (top + childSize.height > parentSize.height) { + top = parentSize.height - childSize.height; + } + return { left, top }; +} +// [End getClampedChildPosition] + +export function showToast( + UIContext: UIContext, + message: ResourceStr = '', + duration = 2000, + alignment = Alignment.Top, + offset: Offset = { dx: 0, dy: 300 } +) { + UIContext.getPromptAction().openToast({ + message, + duration, + alignment, + offset + }); +} + +// [Start calCameraPoint] +export function calCameraPoint(eventX: number, eventY: number, width: number, height: number): camera.Point { + const displayRotation = display.getDefaultDisplaySync().rotation * 90; + if (displayRotation === 0) { + return { x: eventY / height, y: 1 - eventX / width }; + } + if (displayRotation === 90) { + return { x: 1 - eventX / width, y: 1 - eventY / height }; + } + if (displayRotation === 180) { + return { x: 1 - eventY / height, y: eventX / width }; + } + return { x: eventX / width, y: eventY / height }; +} +// [End calCameraPoint] \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/ets/utils/PermissionManager.ets b/CameraKit/DualPreview/entry/src/main/ets/utils/PermissionManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..c65190ea0ecc9ad527f4e576bd6db88a79d4ef40 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/utils/PermissionManager.ets @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 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 { abilityAccessCtrl, Context, Permissions } from '@kit.AbilityKit'; +import { Logger } from 'commons'; + +const TAG = 'PermissionManager'; + +// [Start request_permissions] +class PermissionManager { + private static atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); + + static async request(permissions: Permissions[], context: Context) { + const data = await PermissionManager.atManager.requestPermissionsFromUser(context, permissions); + const grantStatus: number[] = data.authResults; + const deniedPermissions = permissions.filter((_, i) => grantStatus[i] !== 0); + for (const permission of deniedPermissions) { + const secondGrantStatus = await PermissionManager.atManager.requestPermissionOnSetting(context, [permission]); + if (secondGrantStatus[0] !== 0) { + Logger.error(TAG, 'permission denied'); + throw new Error('permission denied'); + } + } + } +} +// [End request_permissions] + +export default PermissionManager; \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/ets/utils/RefreshableTimer.ets b/CameraKit/DualPreview/entry/src/main/ets/utils/RefreshableTimer.ets new file mode 100644 index 0000000000000000000000000000000000000000..4956219f7c1c0ff6d3a80d1c46978f556ca02768 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/utils/RefreshableTimer.ets @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 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. + */ + +// [Start RefreshableTimer] +class RefreshableTimer { + private timerId?: number; + private readonly timeout: number; + private callback: () => void; + private isActive: boolean = false; + + constructor(callback: () => void, timeout: number) { + this.callback = callback; + this.timeout = timeout; + } + + start(): void { + this.timerId = setTimeout(() => { + this.callback(); + this.isActive = false; + }, this.timeout); + this.isActive = true; + } + + clear(): void { + clearTimeout(this.timerId); + this.timerId = undefined; + this.isActive = false; + } + + refresh(): void { + this.clear(); + this.start(); + } + + isRunning(): boolean { + return this.isActive; + } +} +// [End RefreshableTimer] + +export default RefreshableTimer; diff --git a/CameraKit/DualPreview/entry/src/main/ets/utils/WindowUtil.ets b/CameraKit/DualPreview/entry/src/main/ets/utils/WindowUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..9d54a733dca804e4bf57fbf7831e3b4c50f6f284 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/utils/WindowUtil.ets @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 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 { display, window } from '@kit.ArkUI'; +import { Logger } from 'commons'; + +const TAG = 'WindowUtil' + +class WindowUtil { + static async enterImmersive(window: window.Window): Promise { + try { + await window.setWindowLayoutFullScreen(true); + await window.setWindowSystemBarEnable([]); + } catch (exception) { + Logger.error(TAG, 'Failed to enter immersive. Cause:', JSON.stringify(exception)); + } + } + + // Given a ratio, obtain the maximum display width and height based on the screen width and height. + static getMaxDisplaySize(ratio: number): Size { + const defaultDisplay: display.Display = display.getDefaultDisplaySync(); + const windowWidth: number = defaultDisplay.width; + const windowHeight: number = defaultDisplay.height; + const rotation = display.getDefaultDisplaySync().rotation * 90; + if (rotation === 90 || rotation === 270) { + ratio = 1 / ratio; + } + // Calculate the height based on the screen width. + const calculatedHeight = windowWidth * ratio; + if (calculatedHeight <= windowHeight) { + return { + width: windowWidth, + height: calculatedHeight + }; + } else { + return { + width: windowHeight / ratio, + height: windowHeight + }; + } + } + + static getWindowRatio(): number { + const defaultDisplay: display.Display = display.getDefaultDisplaySync(); + return defaultDisplay.height / defaultDisplay.width; + } +} + +export default WindowUtil; \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/ets/viewmodels/PreviewViewModel.ets b/CameraKit/DualPreview/entry/src/main/ets/viewmodels/PreviewViewModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..c277ea257d1d60e83b7a1a068b42d64f2af9ad39 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/viewmodels/PreviewViewModel.ets @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2025 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 { curves, display } from '@kit.ArkUI'; +import { camera } from '@kit.CameraKit'; +import WindowUtil from '../utils/WindowUtil'; +import CameraConstant from '../constants/Constants'; + +export enum CameraMode { + PHOTO, + VIDEO +} + +/** + * States and methods related to preview. + */ +class PreviewViewModel { + private uiContext: UIContext; + // [Start isFront] + isFront: boolean = false; + // [StartExclude isFront] + cameraMode: CameraMode = CameraMode.PHOTO; + xComponentController: XComponentController = new XComponentController(); + surfaceId: string = ''; + previewSize: Size = WindowUtil.getMaxDisplaySize(CameraConstant.RATIO_PHOTO); + rates?: number[] = []; + currentRate: number = 0; + blurRadius: number = 0; + blurRotation: RotateOptions = { y: 0.5, angle: 0}; + + constructor(uiContext: UIContext) { + this.uiContext = uiContext; + } + + // [EndExclude isFront] + getCameraPosition() { + return this.isFront + ? camera.CameraPosition.CAMERA_POSITION_FRONT + : camera.CameraPosition.CAMERA_POSITION_BACK; + } + // [End isFront] + + getPreviewRatio() { + return this.cameraMode === CameraMode.PHOTO + ? CameraConstant.RATIO_PHOTO + : CameraConstant.RATIO_VIDEO; + } + + getSceneMode() { + return this.cameraMode === CameraMode.PHOTO + ? camera.SceneMode.NORMAL_PHOTO + : camera.SceneMode.NORMAL_VIDEO; + } + + // [Start getProfile] + getProfile: (cameraOrientation: number, picFormat: camera.CameraFormat) => camera.Profile = (cameraOrientation, picFormat) => { + const displaySize: Size = WindowUtil.getMaxDisplaySize(this.getPreviewRatio()); + const displayRotation = display.getDefaultDisplaySync().rotation * 90; + const isRevert = (cameraOrientation + displayRotation) % 180 !== 0; + return { + // format: camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP, + format: picFormat, + size: { + height: isRevert ? displaySize.width : displaySize.height, + width: isRevert ? displaySize.height : displaySize.width + } + }; + } + // [End getProfile] + + // [Start setPreviewSize] + setPreviewSize() { + const displaySize: Size = WindowUtil.getMaxDisplaySize(this.getPreviewRatio()); + this.previewSize = displaySize; + this.xComponentController.setXComponentSurfaceRect({ + surfaceWidth: displaySize.width, + surfaceHeight: displaySize.height + }); + } + // [End setPreviewSize] + + getPreviewTop() { + const previewRatio = this.getPreviewRatio(); + return WindowUtil.getWindowRatio() > previewRatio ? 85 : 0; + } + + getPreviewWidth() { + return this.uiContext.px2vp(this.previewSize.width); + } + + getPreviewHeight() { + return this.uiContext.px2vp(this.previewSize.height); + } + + isPhotoMode() { + return this.cameraMode === CameraMode.PHOTO; + } + + isVideoMode() { + return this.cameraMode === CameraMode.VIDEO; + } + + isCurrentCameraMode(mode: CameraMode) { + return this.cameraMode === mode; + } + + openPreviewBlur() { + animateToImmediately({ + duration: 200, + curve: Curve.Friction + }, () => { + this.blurRadius = 150; + }); + } + + rotatePreviewBlur() { + animateToImmediately({ + delay: 50, + duration: 200, + curve: curves.cubicBezierCurve(0.2, 0, 0.83, 1), + onFinish: () => { + this.rotatePreviewBlurSecond(); + } + }, () => { + this.blurRotation = { y: 0.5, angle: 90 }; + }); + } + + rotatePreviewBlurSecond() { + this.blurRotation = { y: 0.5, angle: 270 }; + animateToImmediately({ + duration: 200, + curve: curves.cubicBezierCurve(0.17, 0, 0.2, 1), + onFinish: () => { + this.blurRotation = { y: 0.5, angle: 0 }; + } + }, () => { + this.blurRotation = { y: 0.5, angle: 360 }; + }); + } + + closePreviewBlur() { + animateToImmediately({ + duration: 200, + curve: Curve.FastOutSlowIn + }, () => { + this.blurRadius = 0; + }); + } +} + +export default PreviewViewModel; \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/ets/views/ModeButtonsView.ets b/CameraKit/DualPreview/entry/src/main/ets/views/ModeButtonsView.ets new file mode 100644 index 0000000000000000000000000000000000000000..a1b6494d286415987f161d670d25d5a1da46fb5f --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/views/ModeButtonsView.ets @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 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 { CameraManager, ImageReceiverManager, PhotoManager, VideoManager } from 'camera'; +import PreviewViewModel, { CameraMode } from '../viewmodels/PreviewViewModel'; + +export interface CameraModeButton { + title: ResourceStr; + mode: CameraMode; + onClick?: () => void; +} + +@Component +struct ModeButtonsView { + private cameraModeButtons: CameraModeButton[] = [ + { + title: $r('app.string.photo'), + mode: CameraMode.PHOTO + }, + { + title: $r('app.string.video'), + mode: CameraMode.VIDEO + } + ] + @Link previewVM: PreviewViewModel; // Do not use @prop, otherwise deep copying, some underlying data will be lost. + @Require cameraManager: CameraManager; + @Require photoManager: PhotoManager; + @Require videoManager: VideoManager; + @Require ImageReceiverManager: ImageReceiverManager; + @Require syncButtonSettings: () => void; + + build() { + Row() { + ForEach(this.cameraModeButtons, (modeBtn: CameraModeButton) => { + Text(modeBtn.title) + .fontColor(Color.White) + .fontSize(14) + .fontWeight(this.previewVM.isCurrentCameraMode(modeBtn.mode) ? FontWeight.Bold : FontWeight.Normal) + .onClick(async () => { + if (modeBtn.onClick) { + modeBtn.onClick(); + } else { + if (this.previewVM.isCurrentCameraMode(modeBtn.mode)) { + return; + } + this.previewVM.openPreviewBlur(); + this.previewVM.cameraMode = modeBtn.mode; + this.previewVM.setPreviewSize(); + const sceneMode = this.previewVM.getSceneMode(); + const cameraPosition = this.previewVM.getCameraPosition(); + await this.cameraManager.release(); + this.photoManager.setIsActive(this.previewVM.isPhotoMode() ? true : false); + this.videoManager.setIsActive(this.previewVM.isPhotoMode() ? false : true); + await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); + this.syncButtonSettings(); + } + }) + }, (modeBtn: CameraModeButton) => modeBtn.mode.toString()) + } + .id('modeButtonsView') + .width('40%') + .justifyContent(FlexAlign.SpaceAround) + .alignRules({ + bottom: { anchor: 'operateButtonsView', align: VerticalAlign.Top }, + middle: { anchor: 'operateButtonsView', align: HorizontalAlign.Center } + }) + .margin({ bottom: 40 }) + } +} + +export default ModeButtonsView; \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/ets/views/OperateButtonsView.ets b/CameraKit/DualPreview/entry/src/main/ets/views/OperateButtonsView.ets new file mode 100644 index 0000000000000000000000000000000000000000..8d574d9df9ca7a386e69bca069469c5c70a21a01 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/views/OperateButtonsView.ets @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2025 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 { image } from '@kit.ImageKit'; +import { common } from '@kit.AbilityKit'; +import { AVRecorderState, CameraManager, ImageReceiverManager, PhotoManager, VideoManager } from 'camera'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; + +@Component +struct OperateButtonsView { + @Link isDelayTakePhoto: boolean; + @Link previewVM: PreviewViewModel; + @Require cameraManager: CameraManager; + @Link videoManager: VideoManager; // Do not use @prop, otherwise deep copying, some underlying data will be lost. + @Require photoManager: PhotoManager; + @Require ImageReceiverManager :ImageReceiverManager; + @Prop @Require photoDelayTime: number; + @Link photoRemainder: number; + private photoDelayTimer:number = 0; + @State thumbnail: image.PixelMap | string = ''; + @State thumbnailUrl: string = ''; + @Require syncButtonSettings: () => void; + private context = this.getUIContext().getHostContext() as common.UIAbilityContext; + private setThumbnail: (pixelMap: image.PixelMap, url: string) => void = (pixelMap: image.PixelMap, url: string) => { + this.thumbnail = pixelMap + this.thumbnailUrl = url + } + @StorageLink('captureClick') captureClickFlag: number = 0; + + aboutToAppear(): void { + this.photoManager.setCallback(this.setThumbnail); + this.videoManager.setVideoCallback(this.setThumbnail); + } + + @Builder + photoButton() { + Column() { + Column() + .width(48) + .height(48) + .borderRadius('50%') + .border({ + color: Color.White, + width: 24 + }) + } + .width(64) + .height(64) + .borderRadius('50%') + .border({ + color: Color.White, + width: 1 + }) + .justifyContent(FlexAlign.Center) + .onClick(() => { + if (this.photoDelayTime) { + this.isDelayTakePhoto = true; + this.photoRemainder = this.photoDelayTime; + this.photoDelayTimer = setInterval(()=>{ + this.photoRemainder--; + if(this.photoRemainder === 0){ + this.photoManager.capture(this.previewVM.isFront); + this.captureClickFlag++; + this.isDelayTakePhoto = false; + clearTimeout(this.photoDelayTimer); + } + },1000) + } else { + this.photoManager.capture(this.previewVM.isFront); + this.captureClickFlag++; + } + }) + } + + @Builder + videoStartButton() { + Column() { + Column() + .width(52) + .height(52) + .backgroundColor(Color.Red) + .borderRadius('50%') + .border({ + color: Color.White, + width: 8 + }) + } + .width(64) + .height(64) + .borderRadius('50%') + .border({ + color: Color.White, + width: 1 + }) + .justifyContent(FlexAlign.Center) + .onClick(() => { + this.videoManager.start(this.previewVM.isFront); + }) + } + + @Builder + videoStopButton() { + Column() { + Column() + .width(28) + .height(28) + .borderRadius(10) + .backgroundColor(Color.Red) + } + .width(64) + .height(64) + .borderRadius('50%') + .border({ + color: Color.White, + width: 1 + }) + .justifyContent(FlexAlign.Center) + .onClick(async () => { + if (this.videoManager.state === AVRecorderState.STARTED || this.videoManager.state === AVRecorderState.PAUSED) { + await this.videoManager.stop(); + await this.cameraManager.release(); + const cameraPosition = this.previewVM.getCameraPosition(); + const sceneMode = this.previewVM.getSceneMode(); + await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); + this.syncButtonSettings(); + } + }) + } + + @Builder + videoPauseButton() { + SymbolGlyph($r('sys.symbol.pause')) + .fontColor([Color.White]) + .fontSize(22) + .fontWeight(FontWeight.Bolder) + .padding(11) + .borderWidth(1) + .borderColor(Color.White) + .borderRadius('50%') + .symbolEffect(new ReplaceSymbolEffect(EffectScope.WHOLE), true) + .onClick(async () => { + this.videoManager.pause(); + }) + } + + @Builder + videoResumeButton() { + SymbolGlyph($r('sys.symbol.play')) + .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR) + .fontColor([Color.White]) + .fontSize(22) + .padding(11) + .borderWidth(1) + .borderColor(Color.White) + .borderRadius('50%') + .onClick(async () => { + this.videoManager.resume(); + }) + } + + @Builder + thumbnailButton() { + Image(this.thumbnail) + .width(48) + .height(48) + .borderRadius('50%') + .borderWidth(this.thumbnail ? 1 : 0) + .borderColor(Color.White) + .clip(true) + .onClick(()=>{ + if(this.thumbnailUrl){ + this.context.startAbility({ + parameters: { uri: this.thumbnailUrl }, + action: 'ohos.want.action.viewData', + bundleName: 'com.huawei.hmos.photos', + abilityName: 'com.huawei.hmos.photos.MainAbility' + }); + } + }) + } + + @Builder + delayTakePhotoCancelButton() { + SymbolGlyph($r('sys.symbol.pause')) + .fontColor([Color.White]) + .fontSize(22) + .fontWeight(FontWeight.Bolder) + .padding(11) + .borderWidth(1) + .borderColor(Color.White) + .borderRadius('50%') + .symbolEffect(new ReplaceSymbolEffect(EffectScope.WHOLE), true) + .onClick(async () => { + this.isDelayTakePhoto = false; + clearTimeout(this.photoDelayTimer); + this.photoRemainder = 0; + }) + } + + // [Start toggleCameraPositionButton] + @Builder + toggleCameraPositionButton() { + Image($r('app.media.toggle_position')) + .width(48) + .height(48) + .onClick(async () => { + // [StartExclude toggleCameraPositionButton] + this.previewVM.openPreviewBlur(); + this.previewVM.rotatePreviewBlur(); + // [EndExclude toggleCameraPositionButton] + this.previewVM.isFront = !this.previewVM.isFront; + const cameraPosition = this.previewVM.getCameraPosition(); + const sceneMode = this.previewVM.getSceneMode(); + await this.cameraManager.release(); + await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); + // [StartExclude toggleCameraPositionButton] + this.syncButtonSettings(); + // [EndExclude toggleCameraPositionButton] + }) + } + // [End toggleCameraPositionButton] + + build() { + Row() { + // this.thumbnailButton() + // if (this.previewVM.isPhotoMode()) { + // this.photoButton() + // } else { + // if (this.videoManager.isRecording()) { + // this.videoStopButton() + // } else { + // this.videoStartButton() + // } + // } + if (!this.videoManager.isRecording()) { + this.toggleCameraPositionButton() + } + // if (this.previewVM.isVideoMode() && this.videoManager.state === AVRecorderState.STARTED) { + // this.videoPauseButton() + // } else if (this.previewVM.isVideoMode() && this.videoManager.state === AVRecorderState.PAUSED) { + // this.videoResumeButton() + // } + } + .justifyContent(FlexAlign.SpaceAround) + .id('operateButtonsView') + .alignRules({ + bottom: { anchor: '__container__', align: VerticalAlign.Bottom }, + middle: { anchor: '__container__', align: HorizontalAlign.Center }, + }) + .margin({ bottom: 90 }) + .width('90%') + } +} + +export default OperateButtonsView; \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/ets/views/SettingButtonsView.ets b/CameraKit/DualPreview/entry/src/main/ets/views/SettingButtonsView.ets new file mode 100644 index 0000000000000000000000000000000000000000..fdc7f91abb2aef20dd8ba98a52ba8fc50c63aa07 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/views/SettingButtonsView.ets @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2025 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 '@kit.CameraKit'; +import { AVRecorderState, CameraManager, PhotoManager, PreviewManager, VideoManager } from 'camera'; +import { showToast } from '../utils/CommonUtil'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; + +interface FlashItem { + mode: camera.FlashMode; + image: Resource; + title: ResourceStr; + toast: ResourceStr; +} + +@Component +struct SettingButtonsView { + private flashItems: FlashItem[] = [ + { + mode: camera.FlashMode.FLASH_MODE_CLOSE, + image: $r('sys.symbol.camera_flash_slash'), + title: 'off', + toast: $r('app.string.flash_close') + }, + { + mode: camera.FlashMode.FLASH_MODE_OPEN, + image: $r('sys.symbol.camera_flash'), + title: 'on', + toast: $r('app.string.flash_open') + }, + { + mode: camera.FlashMode.FLASH_MODE_AUTO, + image: $r('sys.symbol.camera_flash_auto'), + title: 'auto', + toast: $r('app.string.flash_auto') + }, + { + mode: camera.FlashMode.FLASH_MODE_ALWAYS_OPEN, + image: $r('sys.symbol.lightbulb_1'), + title: 'always_on', + toast: $r('app.string.flash_always') + } + ]; + private photoFlashModes: camera.FlashMode[] = [ + camera.FlashMode.FLASH_MODE_CLOSE, + camera.FlashMode.FLASH_MODE_OPEN, + camera.FlashMode.FLASH_MODE_AUTO, + camera.FlashMode.FLASH_MODE_ALWAYS_OPEN + ]; + private videoFlashModes: camera.FlashMode[] = [ + camera.FlashMode.FLASH_MODE_CLOSE, + camera.FlashMode.FLASH_MODE_ALWAYS_OPEN + ]; + @State flashMode: camera.FlashMode = camera.FlashMode.FLASH_MODE_CLOSE; + @Link isLivePhoto: boolean; + @Require cameraManager: CameraManager; + @Require previewManager: PreviewManager; + @Require photoManager: PhotoManager; + @Link videoManager: VideoManager; + @Link photoDelayTime: number; + @State isStabilizationEnabled: boolean = false; + @Link isSinglePhoto: boolean; + @Link previewVM: PreviewViewModel; + + getFlashItem(mode: camera.FlashMode) { + return this.flashItems.find(item => item.mode === mode); + } + + @Builder + flashButton(flashModes: camera.FlashMode[]) { + SymbolGlyph(this.getFlashItem(this.flashMode)?.image) + .fontSize(22) + .fontColor([Color.White]) + .bindMenu(flashModes.map(mode => { + const flashItem = this.getFlashItem(mode)!; + const menuElement: MenuElement = { + value: flashItem.title, + action: () => { + this.flashMode = mode!; + this.cameraManager.setFlashMode(mode); + showToast(this.getUIContext(), flashItem.toast); + } + }; + return menuElement; + })) + } + + @Builder + videoTimerBuilder() { + if (this.videoManager.isRecording()) { + Row({ space: 5 }) { + SymbolGlyph(this.videoManager.state === AVRecorderState.STARTED ? $r('sys.symbol.record_circle') : + $r('sys.symbol.pause')) + .fontSize(22) + .fontColor(this.videoManager.state === AVRecorderState.STARTED ? [Color.Red, 'rgba(255,0,0,0)'] : + [Color.White]) + .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR) + Text(this.videoManager.state === AVRecorderState.STARTED ? $r('app.string.recording') : $r('app.string.paused')) + .fontColor(Color.White) + .fontSize(12) + } + } + } + + @Builder + livePhotoButton() { + SymbolGlyph(this.isLivePhoto + ? $r('sys.symbol.livephoto') + : $r('sys.symbol.livephoto_slash')) + .onClick(() => { + this.isLivePhoto = !this.isLivePhoto; + this.photoManager.enableMovingPhoto(this.isLivePhoto); + const message = this.isLivePhoto ? $r('app.string.moving_open') : $r('app.string.moving_close'); + showToast(this.getUIContext(), message); + }) + .fontSize(22) + .fontColor([Color.White]) + } + + @Builder + rateButton() { + if (this.previewVM.rates) { + Text(`${this.previewVM.currentRate} fps`) + .fontColor(Color.White) + .fontSize(16) + .bindMenu(this.previewVM.rates.map(rate => { + const menuElement: MenuElement = { + value: rate + 'fps', + action: () => { + this.previewManager.setFrameRate(rate, rate); + this.previewVM.currentRate = rate; + showToast(this.getUIContext(), $r('app.string.preview_rate', rate + 'fps')); + } + }; + return menuElement; + })) + } + } + + @Builder + delayPhotoButton(photoDelayTimeElements: MenuElement[]) { + if (this.photoDelayTime) { + Text(`${this.photoDelayTime}s`) + .fontColor(Color.White) + .fontSize(16) + .bindMenu(photoDelayTimeElements) + } else { + SymbolGlyph($r('sys.symbol.time_lapse')) + .fontSize(22) + .fontColor([Color.White]) + .bindMenu(photoDelayTimeElements) + } + } + + getPhotoDelayTimeElements(): MenuElement[] { + const menuTextMap: Map = new Map([ + ['off', 0], + ['2s', 2], + ['5s', 5], + ['10s', 10] + ]) + return Array.from(menuTextMap.keys()).map(text => { + const time = menuTextMap.get(text); + const menuElement: MenuElement = { + value: text, + action: () => { + this.photoDelayTime = time!; + const message = time ? $r('app.string.delay', text) : $r('app.string.delay_close'); + showToast(this.getUIContext(), message); + } + }; + return menuElement; + }); + } + + @Builder + stabilizationButton() { + SymbolGlyph(this.isStabilizationEnabled + ? $r('sys.symbol.motion_stabilization') + : $r('sys.symbol.motion_stabilization_slash')) + .onClick(() => { + this.isStabilizationEnabled = !this.isStabilizationEnabled; + this.cameraManager.setVideoStabilizationMode(camera.VideoStabilizationMode.AUTO); + const message = this.isStabilizationEnabled ? $r('app.string.stabilization_enable') : $r('app.string.stabilization_disabled'); + // showToast(this.getUIContext(), message); + }) + .fontSize(22) + .fontColor([Color.White]) + } + + @Builder + togglePhotoModeButton() { + SymbolGlyph(this.isSinglePhoto + ? $r('sys.symbol.picture') + : $r('sys.symbol.picture_on_square_1')) + .onClick(() => { + this.isSinglePhoto = !this.isSinglePhoto; + this.photoManager.setPhotoOutputCallback(this.isSinglePhoto); + if (this.isSinglePhoto) { + this.isLivePhoto = false; + } + this.photoManager.enableMovingPhoto(this.isLivePhoto); + const message = this.isSinglePhoto ? $r('app.string.photo_single') : $r('app.string.photo_double'); + showToast(this.getUIContext(), message); + }) + .fontSize(22) + .fontColor([Color.White]) + } + + build() { + Row() { + if (this.previewVM.isPhotoMode()) { + //this.rateButton() + this.flashButton(this.photoFlashModes) + //this.delayPhotoButton(this.getPhotoDelayTimeElements()) + // if (!this.isSinglePhoto) { + // this.livePhotoButton() + // } + //this.togglePhotoModeButton() + } else { + if (this.videoManager.isRecording()) { + //this.videoTimerBuilder() + } else { + //this.rateButton() + this.flashButton(this.videoFlashModes) + //this.stabilizationButton() + } + } + } + .id('settingButtonsView') + .width('100%') + .margin({ top: 50 }) + .justifyContent(FlexAlign.SpaceAround) + .alignRules({ top: { anchor: '__container__', align: VerticalAlign.Top } }) + } +} + +export default SettingButtonsView; \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/ets/views/ZoomButtonsView.ets b/CameraKit/DualPreview/entry/src/main/ets/views/ZoomButtonsView.ets new file mode 100644 index 0000000000000000000000000000000000000000..ccdef4750b5fa7025cb7e6a40a42a97b6cb54012 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/ets/views/ZoomButtonsView.ets @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 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 { CameraManager } from 'camera'; +import { findRangeIndex, toFixed } from '../utils/CommonUtil'; + +@Component +struct ZoomButtonsView { + @Prop @Require zooms: number[]; + @Prop @Require zoomRange: number[] = []; + @Link currentZoom: number; + @Require cameraManager: CameraManager; + + getZoomButtonText(zoom: number, index: number): string { + const minZoom = this.zoomRange[0]; + const currentZoomIndex: number = findRangeIndex(this.currentZoom, this.zooms); + if (index === 0 && (this.currentZoom === minZoom || currentZoomIndex !== index)) { + return 'w'; + } + if (this.currentZoom === zoom || currentZoomIndex !== index) { + return `${zoom}x`; + } + return `${toFixed(this.currentZoom, 1)}x`; + } + + getZoomButtonBorderWidth(index: number): number { + const currentZoomIndex: number = findRangeIndex(this.currentZoom, this.zooms); + return currentZoomIndex === index ? 1.5 : 0; + } + + build() { + Row({ space: 15 }) { + ForEach(this.zooms, (zoom: number, index: number) => { + Text(this.getZoomButtonText(zoom, index)) + .width(36) + .height(36) + .fontColor(Color.White) + .fontSize(10) + .fontWeight(FontWeight.Bold) + .borderWidth(this.getZoomButtonBorderWidth(index)) + .borderColor(Color.White) + .borderRadius('50%') + .textAlign(TextAlign.Center) + .onClick(() => { + this.cameraManager.setSmoothZoom(zoom) + this.currentZoom = zoom + }) + }, (zoom: number) => zoom.toString()) + } + .margin({ bottom: 40 }) + .alignRules({ + bottom: { anchor: 'modeButtonsView', align: VerticalAlign.Top }, + middle: { anchor: 'modeButtonsView', align: HorizontalAlign.Center } + }) + } +} +export default ZoomButtonsView; \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/module.json5 b/CameraKit/DualPreview/entry/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..01eaa8b29b5deabd3dfb66e6a99010ffce78bfc5 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/module.json5 @@ -0,0 +1,110 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "phone", + "tablet" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ], + // [Start permissions] + // [Start permissions_acc] + "requestPermissions": [ + // [StartExclude permissions_acc] + { + "name": "ohos.permission.CAMERA", + "reason": "$string:permission_CAMERA", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + // [StartExclude permissions] + { + "name": "ohos.permission.MICROPHONE", + "reason": "$string:reason_microphone", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + { + "name": "ohos.permission.MEDIA_LOCATION", + "reason": "$string:reason_media_location", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + { + "name": "ohos.permission.WRITE_IMAGEVIDEO", + "reason": "$string:reason_write_imagevideo", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + { + "name": "ohos.permission.READ_IMAGEVIDEO", + "reason": "$string:reason_read_imagevideo", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + }, + }, + { + "name": "ohos.permission.APPROXIMATELY_LOCATION", + "reason": "$string:permission_LOCATION", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + // [EndExclude permissions_acc] + { + "name": "ohos.permission.ACCELEROMETER", + "reason": "$string:permission_SENSOR", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + } + // [EndExclude permissions] + ] + // [End permissions] + // [End permissions_acc] + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/element/color.json b/CameraKit/DualPreview/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..3c712962da3c2751c2b9ddb53559afcbd2b54a02 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/element/float.json b/CameraKit/DualPreview/entry/src/main/resources/base/element/float.json new file mode 100644 index 0000000000000000000000000000000000000000..33ea22304f9b1485b5f22d811023701b5d4e35b6 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/resources/base/element/float.json @@ -0,0 +1,8 @@ +{ + "float": [ + { + "name": "page_text_font_size", + "value": "50fp" + } + ] +} diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/element/string.json b/CameraKit/DualPreview/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..32d0de3f681b25d4009cffd841f63483b0e3a0d8 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/resources/base/element/string.json @@ -0,0 +1,140 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "CustomCamera" + }, + { + "name": "permission_CAMERA", + "value": "For camera operations" + }, + { + "name": "reason_microphone", + "value": "For camera video recording" + }, + { + "name": "reason_media_location", + "value": "For scenarios where the camera retrieves media information" + }, + { + "name": "reason_write_imagevideo", + "value": "For reading and writing media files with the camera" + }, + { + "name": "reason_read_imagevideo", + "value": "For camera media file read/write" + }, + { + "name": "permission_SENSOR", + "value": "For sensors" + }, + { + "name": "recording", + "value": "Recording in progress" + }, + { + "name": "paused", + "value": "Paused" + }, + { + "name": "wakeup_text", + "value": "Tap the screen to wake up the camera" + }, + { + "name": "photo", + "value": "Photo" + }, + { + "name": "video", + "value": "Video" + }, + { + "name": "preview_rate", + "value": "Preview frame rate%d" + }, + { + "name": "delay", + "value": "Time-lapse%d" + }, + { + "name": "delay_close", + "value": "Off time-lapse" + }, + { + "name": "moving_open", + "value": "Live photo mode is on" + }, + { + "name": "moving_close", + "value": "Live photo mode is off" + }, + { + "name": "photo_single", + "value": "Single-segment shooting" + }, + { + "name": "photo_double", + "value": "Dual-segment shooting" + }, + { + "name": "stabilization_enable", + "value": "Video stabilization is on" + }, + { + "name": "stabilization_disabled", + "value": "Video stabilization is off" + }, + { + "name": "grid_line_open", + "value": "Grid lines are enabled" + }, + { + "name": "grid_line_close", + "value": "Grid lines are disabled" + }, + { + "name": "level_open", + "value": "The spirit level is enabled" + }, + { + "name": "level_close", + "value": "The spirit level is disabled" + }, + { + "name": "flash_auto", + "value": "Flash is in auto mode" + }, + { + "name": "flash_close", + "value": "The flash is off" + }, + { + "name": "flash_always", + "value": "The flash is always on" + }, + { + "name": "flash_open", + "value": "The flash is on" + }, + { + "name": "preview_image_open", + "value": "Dual-channel preview has been enabled" + }, + { + "name": "preview_image_close", + "value": "Dual-channel preview has been disabled" + }, + { + "name": "permission_LOCATION", + "value": "For location" + } + ] +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/media/background.png b/CameraKit/DualPreview/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f Binary files /dev/null and b/CameraKit/DualPreview/entry/src/main/resources/base/media/background.png differ diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/media/flash_10s.png b/CameraKit/DualPreview/entry/src/main/resources/base/media/flash_10s.png new file mode 100644 index 0000000000000000000000000000000000000000..b6de041fe17fa2dc0ff7a6784e0bee0526e2277f Binary files /dev/null and b/CameraKit/DualPreview/entry/src/main/resources/base/media/flash_10s.png differ diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/media/flash_2s.png b/CameraKit/DualPreview/entry/src/main/resources/base/media/flash_2s.png new file mode 100644 index 0000000000000000000000000000000000000000..183c0ec5c8b99261dbf6130373cb60b08534f672 Binary files /dev/null and b/CameraKit/DualPreview/entry/src/main/resources/base/media/flash_2s.png differ diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/media/flash_5s.png b/CameraKit/DualPreview/entry/src/main/resources/base/media/flash_5s.png new file mode 100644 index 0000000000000000000000000000000000000000..1822496b37069f119060e789f1228cea746e1327 Binary files /dev/null and b/CameraKit/DualPreview/entry/src/main/resources/base/media/flash_5s.png differ diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/media/focus_box.png b/CameraKit/DualPreview/entry/src/main/resources/base/media/focus_box.png new file mode 100644 index 0000000000000000000000000000000000000000..7a5d9b649e08465de0e8e8883a9a37d7c3bb9f36 Binary files /dev/null and b/CameraKit/DualPreview/entry/src/main/resources/base/media/focus_box.png differ diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/media/foreground.png b/CameraKit/DualPreview/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..97014d3e10e5ff511409c378cd4255713aecd85f Binary files /dev/null and b/CameraKit/DualPreview/entry/src/main/resources/base/media/foreground.png differ diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/media/layered_image.json b/CameraKit/DualPreview/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/media/startIcon.png b/CameraKit/DualPreview/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..205ad8b5a8a42e8762fbe4899b8e5e31ce822b8b Binary files /dev/null and b/CameraKit/DualPreview/entry/src/main/resources/base/media/startIcon.png differ diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/media/toggle_position.png b/CameraKit/DualPreview/entry/src/main/resources/base/media/toggle_position.png new file mode 100644 index 0000000000000000000000000000000000000000..0843141ece2b81e910903dcd2b153f37ee4bf612 Binary files /dev/null and b/CameraKit/DualPreview/entry/src/main/resources/base/media/toggle_position.png differ diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/profile/backup_config.json b/CameraKit/DualPreview/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000000000000000000000000000000000000..78f40ae7c494d71e2482278f359ec790ca73471a --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/resources/base/profile/main_pages.json b/CameraKit/DualPreview/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000000000000000000000000000000000..1898d94f58d6128ab712be2c68acc7c98e9ab9ce --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/CameraKit/DualPreview/entry/src/main/resources/dark/element/color.json b/CameraKit/DualPreview/entry/src/main/resources/dark/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..79b11c2747aec33e710fd3a7b2b3c94dd9965499 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/resources/dark/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/resources/en_US/element/string.json b/CameraKit/DualPreview/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..84573dc11ed7df2080447c4a2dcd36c44ccccd45 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,140 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "CustomCamera" + }, + { + "name": "permission_CAMERA", + "value": "For camera operations" + }, + { + "name": "reason_microphone", + "value": "For camera video recording" + }, + { + "name": "reason_media_location", + "value": "For scenarios where the camera retrieves media information" + }, + { + "name": "reason_write_imagevideo", + "value": "For reading and writing media files with the camera" + }, + { + "name": "reason_read_imagevideo", + "value": "For camera media file read/write" + }, + { + "name": "permission_SENSOR", + "value": "For sensors" + }, + { + "name": "recording", + "value": "Recording in progress" + }, + { + "name": "paused", + "value": "Paused" + }, + { + "name": "wakeup_text", + "value": "Tap the screen to wake up the camera" + }, + { + "name": "photo", + "value": "Photo" + }, + { + "name": "video", + "value": "Video" + }, + { + "name": "preview_rate", + "value": "Preview frame rate%d" + }, + { + "name": "delay", + "value": "Time-lapse%d" + }, + { + "name": "delay_close", + "value": "Off time-lapse" + }, + { + "name": "moving_open", + "value": "Live photo mode is on" + }, + { + "name": "moving_close", + "value": "Live photo mode is off" + }, + { + "name": "photo_single", + "value": "Single-segment shooting" + }, + { + "name": "photo_double", + "value": "Dual-segment shooting" + }, + { + "name": "stabilization_enable", + "value": "Video stabilization is on" + }, + { + "name": "stabilization_disabled", + "value": "Video stabilization is off" + }, + { + "name": "grid_line_open", + "value": "Grid lines are enabled." + }, + { + "name": "grid_line_close", + "value": "Grid lines are disabled." + }, + { + "name": "level_open", + "value": "The spirit level is enabled" + }, + { + "name": "level_close", + "value": "The spirit level is disabled" + }, + { + "name": "flash_auto", + "value": "Flash is in auto mode" + }, + { + "name": "flash_close", + "value": "The flash is off." + }, + { + "name": "flash_always", + "value": "The flash is always on" + }, + { + "name": "flash_open", + "value": "The flash is on" + }, + { + "name": "preview_image_open", + "value": "Dual-channel preview has been enabled" + }, + { + "name": "preview_image_close", + "value": "Dual-channel preview has been disabled" + }, + { + "name": "permission_LOCATION", + "value": "For location" + } + ] +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/main/resources/zh_CN/element/string.json b/CameraKit/DualPreview/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..fc4e395ef928cabd2525ab4101ac5ee6fdd018ca --- /dev/null +++ b/CameraKit/DualPreview/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,140 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "CameraKitSample" + }, + { + "name": "permission_CAMERA", + "value": "用于相机操作" + }, + { + "name": "reason_microphone", + "value": "用于相机录像场景" + }, + { + "name": "reason_media_location", + "value": "用于相机获取媒体信息场景" + }, + { + "name": "reason_write_imagevideo", + "value": "用于相机读写媒体文件" + }, + { + "name": "reason_read_imagevideo", + "value": "用于相机读写媒体文件" + }, + { + "name": "permission_SENSOR", + "value": "用于传感器" + }, + { + "name": "recording", + "value": "录制中" + }, + { + "name": "paused", + "value": "已暂停" + }, + { + "name": "wakeup_text", + "value": "点击屏幕唤醒相机" + }, + { + "name": "photo", + "value": "拍照" + }, + { + "name": "video", + "value": "录像" + }, + { + "name": "preview_rate", + "value": "预览帧率%d" + }, + { + "name": "delay", + "value": "延时拍照%d" + }, + { + "name": "delay_close", + "value": "延时拍照关闭" + }, + { + "name": "moving_open", + "value": "动态拍照已开启" + }, + { + "name": "moving_close", + "value": "动态拍照已关闭" + }, + { + "name": "photo_single", + "value": "单段拍照" + }, + { + "name": "photo_double", + "value": "双段拍照" + }, + { + "name": "stabilization_enable", + "value": "录像防抖已开启" + }, + { + "name": "stabilization_disabled", + "value": "录像防抖已关闭" + }, + { + "name": "grid_line_open", + "value": "网格线已开启" + }, + { + "name": "grid_line_close", + "value": "网格线已关闭" + }, + { + "name": "level_open", + "value": "水平仪已开启" + }, + { + "name": "level_close", + "value": "水平仪已关闭" + }, + { + "name": "flash_auto", + "value": "闪关灯已自动" + }, + { + "name": "flash_close", + "value": "闪光灯已关闭" + }, + { + "name": "flash_always", + "value": "闪光灯已常亮" + }, + { + "name": "flash_open", + "value": "闪光灯已开启" + }, + { + "name": "preview_image_open", + "value": "双路预览已开启" + }, + { + "name": "preview_image_close", + "value": "双路预览已关闭" + }, + { + "name": "permission_LOCATION", + "value": "用于地理位置" + } + ] +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/ohosTest/ets/test/Ability.test.ets b/CameraKit/DualPreview/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..b5f95e765dbee7c650b4f75791aa58eb1009de34 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025 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 AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import Base from '@ohos.base'; +import abilityAccessCtrl from '@ohos.abilityAccessCtrl'; +import { PermissionRequestResult } from '@ohos.abilityAccessCtrl'; +import { Permissions } from '@kit.AbilityKit'; +import { Driver, ON, MatchPattern, Component } from '@ohos.UiTest'; +import fs from '@ohos.file.fs'; +import Want from '@ohos.app.ability.Want'; + +const TAG = 'abilityTest'; +const domain: number = 0x0000; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('captureTest', 0, async () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + try { + let want: Want = { + bundleName: 'com.example.CameraKit', + abilityName: 'EntryAbility' + }; + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + abilityDelegator.startAbility(want, (err: Base.BusinessError) => { + hilog.info(domain, TAG, 'StartAbility get err ' + JSON.stringify(err)); + // expect(err).assertNull(); + }) + + console.info('%{public}s', ' beforeAll 4'); + let driver = Driver.create(); + await driver.delayMs(1000); + let permissionButton: Component | null = null; + for (let i = 0; i < 3; i++) { + permissionButton = await driver.waitForComponent(ON.text('允许', MatchPattern.EQUALS), 1000); + console.info('captureTest btn1' + JSON.stringify(permissionButton)); + if (permissionButton != null) { + await permissionButton.click(); + console.info('captureTest after permissionButton.click '); + await driver.delayMs(300); // 等待下一个权限请求弹窗出现 + } + } + permissionButton = await driver.findComponent(ON.text('仅使用期间允许', MatchPattern.EQUALS)); + await permissionButton.click(); + await driver.delayMs(300); // 等待下一个权限请求弹窗出现 + await driver.delayMs(300); + console.info('%{public}s', ' beforeAll 6'); + let btn1 = await driver.findComponent(ON.text('拍照', MatchPattern.EQUALS)); + console.info('captureTest btn1' + JSON.stringify(btn1)); + await btn1.click(); + await driver.delayMs(300); + console.info('%{public}s', ' beforeAll 8'); + let btn2 = await driver.findComponent(ON.type('Column').id('Capture')); + console.info('captureTest btn2' + JSON.stringify(btn2)); + await btn2.click(); + console.info('%{public}s', ' beforeAll 10'); + await driver.delayMs(3000); + expect(btn2 == null).assertFalse(); + await driver.delayMs(1000); + } catch (err) { + console.info(' captureTest failed, err: ' + err); + } + }) + + it('recordTest', 0, async () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + try { + let driver = Driver.create(); + await driver.delayMs(300); + let btn1 = await driver.findComponent(ON.text('录像', MatchPattern.EQUALS)); + console.info('recordTest btn1' + JSON.stringify(btn1)); + await btn1.click(); + await driver.delayMs(300); + console.info('%{public}s', ' recordTest 8'); + let btn2 = await driver.findComponent(ON.type('Column').id('VideoStartButton')); + console.info('recordTest btn2' + JSON.stringify(btn2)); + await btn2.click(); + console.info('%{public}s', ' recordTest 10'); + await driver.delayMs(3000); + let btn3 = await driver.findComponent(ON.type('Column').id('VideoStopButton')); + console.info('recordTest btn3' + JSON.stringify(btn3)); + expect(btn3 == null).assertFalse(); + await btn3.click(); + await driver.delayMs(500); + let btn4 = await driver.findComponent(ON.type('Image').id('Thumbnail')); + console.info('recordTest btn4' + JSON.stringify(btn4)); + await btn4.click(); + await driver.delayMs(500); + } catch (err) { + console.info(' recordTest failed, err: ' + err); + } + }) + }) +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/ohosTest/ets/test/List.test.ets b/CameraKit/DualPreview/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..6068cbde604813d2160e3a433ac144904769ee3a --- /dev/null +++ b/CameraKit/DualPreview/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,6 @@ +import abilityTest from './Ability.test'; + + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/ohosTest/module.json5 b/CameraKit/DualPreview/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..69026872775eebd0844900b225c411959ae5608b --- /dev/null +++ b/CameraKit/DualPreview/entry/src/ohosTest/module.json5 @@ -0,0 +1,12 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "deviceTypes": [ + "default", + "tablet" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/CameraKit/DualPreview/entry/src/test/List.test.ets b/CameraKit/DualPreview/entry/src/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..bb5b5c3731e283dd507c847560ee59bde477bbc7 --- /dev/null +++ b/CameraKit/DualPreview/entry/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/CameraKit/DualPreview/entry/src/test/LocalUnit.test.ets b/CameraKit/DualPreview/entry/src/test/LocalUnit.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..165fc1615ee8618b4cb6a622f144a9a707eee99f --- /dev/null +++ b/CameraKit/DualPreview/entry/src/test/LocalUnit.test.ets @@ -0,0 +1,33 @@ +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function localUnitTest() { + describe('localUnitTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }); + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }); + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }); + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }); + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }); + }); +} \ No newline at end of file diff --git a/CameraKit/DualPreview/hvigor/hvigor-config.json5 b/CameraKit/DualPreview/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..5bebc9755447385d82ce4138f54d991b1f85f348 --- /dev/null +++ b/CameraKit/DualPreview/hvigor/hvigor-config.json5 @@ -0,0 +1,22 @@ +{ + "modelVersion": "5.0.5", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/CameraKit/DualPreview/hvigorfile.ts b/CameraKit/DualPreview/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3cb9f1a87a81687554a76283af8df27d8bda775 --- /dev/null +++ b/CameraKit/DualPreview/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/CameraKit/DualPreview/oh-package-lock.json5 b/CameraKit/DualPreview/oh-package-lock.json5 new file mode 100644 index 0000000000000000000000000000000000000000..9f659f79cdaf13e186013d40e3a0ba54174de6da --- /dev/null +++ b/CameraKit/DualPreview/oh-package-lock.json5 @@ -0,0 +1,20 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hypium@1.0.18": "@ohos/hypium@1.0.18" + }, + "packages": { + "@ohos/hypium@1.0.18": { + "name": "@ohos/hypium", + "version": "1.0.18", + "integrity": "sha512-RGe/iLGdeywdQilMWZsHKUoiE9OJ+9QxQsorF92R2ImLNVHVhbpSePNITGpW7TnvLgOIP/jscOqfIOhk6X7XRQ==", + "resolved": "https://ohpm.openharmony.cn/ohpm/@ohos/hypium/-/hypium-1.0.18.har", + "registryType": "ohpm" + } + } +} \ No newline at end of file diff --git a/CameraKit/DualPreview/oh-package.json5 b/CameraKit/DualPreview/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..114bdf65f7a3a7157b50109f09e7a0edcf44e832 --- /dev/null +++ b/CameraKit/DualPreview/oh-package.json5 @@ -0,0 +1,9 @@ +{ + "modelVersion": "5.0.5", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.18" + } +} diff --git a/CameraKit/DualPreview/screenshots/devices/camera.PNG b/CameraKit/DualPreview/screenshots/devices/camera.PNG new file mode 100644 index 0000000000000000000000000000000000000000..d8a7933d3c382c2286e357c446c662ccde1cbd7e Binary files /dev/null and b/CameraKit/DualPreview/screenshots/devices/camera.PNG differ diff --git a/CameraKit/RotationDisplayCamera/.gitignore b/CameraKit/RotationDisplayCamera/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d2ff20141ceed86d87c0ea5d99481973005bab2b --- /dev/null +++ b/CameraKit/RotationDisplayCamera/.gitignore @@ -0,0 +1,12 @@ +/node_modules +/oh_modules +/local.properties +/.idea +**/build +/.hvigor +.cxx +/.clangd +/.clang-format +/.clang-tidy +**/.test +/.appanalyzer \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/AppScope/app.json5 b/CameraKit/RotationDisplayCamera/AppScope/app.json5 new file mode 100644 index 0000000000000000000000000000000000000000..4fb9f79d7ead7eb0e2dcd87425bcfddd0535f158 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.example.CameraKit", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:layered_image", + "label": "$string:app_name" + } +} diff --git a/CameraKit/RotationDisplayCamera/AppScope/resources/base/element/string.json b/CameraKit/RotationDisplayCamera/AppScope/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..dcff7a161c7e37223d7fa221466db8b975b499a8 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "CustomCamera" + } + ] +} diff --git a/CameraKit/RotationDisplayCamera/AppScope/resources/base/media/background.png b/CameraKit/RotationDisplayCamera/AppScope/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f Binary files /dev/null and b/CameraKit/RotationDisplayCamera/AppScope/resources/base/media/background.png differ diff --git a/CameraKit/RotationDisplayCamera/AppScope/resources/base/media/foreground.png b/CameraKit/RotationDisplayCamera/AppScope/resources/base/media/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..97014d3e10e5ff511409c378cd4255713aecd85f Binary files /dev/null and b/CameraKit/RotationDisplayCamera/AppScope/resources/base/media/foreground.png differ diff --git a/CameraKit/RotationDisplayCamera/AppScope/resources/base/media/layered_image.json b/CameraKit/RotationDisplayCamera/AppScope/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/CameraKit/RotationDisplayCamera/AppScope/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/LICENSE b/CameraKit/RotationDisplayCamera/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..18795a48d6b12fcdc1aa7bac9a9cb99f83815267 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/LICENSE @@ -0,0 +1,78 @@ + Copyright (c) 2025 Huawei Device Co., Ltd. All rights reserved. + + 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. + +Apache License, Version 2.0 +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: +1.You must give any other recipients of the Work or Derivative Works a copy of this License; and +2.You must cause any modified files to carry prominent notices stating that You changed the files; and +3.You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +4.If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/README.md b/CameraKit/RotationDisplayCamera/README.md new file mode 100644 index 0000000000000000000000000000000000000000..15b7260e12be534e3cc572bed040ebae55d22958 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/README.md @@ -0,0 +1,88 @@ +## 实现相机旋转功能 + +### 介绍 + +本示例基于Camera Kit相机服务,使用ArkTS API实现基础预览、预览画面调整(前后置镜头切换、闪光灯、对焦、调焦、设置曝光中心点等)、预览进阶功能(网格线、水平仪、超时暂停等)、双路预览(获取预览帧数据)、拍照(动图拍摄、延迟拍摄等)、录像等核心功能。 +在前述功能的基础上,并[适配相机旋转角度](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/camera-rotation-angle-adaptation)。 +为开发者提供自定义相机开发的完整参考与实践指导。 + +### 效果预览 + +![](./screenshots/devices/camera.png) + +使用说明: +1. 打开应用,授权后展示预览界面。 +2. 上方从左至右按钮功能依次为:闪光灯设置、延迟拍照模式设置、动态拍照模式设置、单双段拍照模式设置(单段拍照模式不支持动态拍摄)。 +3. 切换录像模式,上方按钮依次为:闪关灯设置、防抖模式设置。 +4. 右侧按钮依次为:网格线、水平仪、双路预览(获取预览帧数据)。 +5. 下方按钮可拍照,录像,切换前后置摄像头。 +6. 若设备支持折叠屏,在展开态时,关闭旋转锁定,旋转设备,则预览画面实现对应旋转。 + +### 工程目录 + +``` +├──camera/src/ +│ ├──main/ets/ +│ │ ├──components +│ │ │ ├──GridLine.ets // 网格线组件 +│ │ │ └──LevelIndicator.ets // 水平仪组件 +│ │ ├──constants +│ │ │ └──CameraConstants.ets // 常量文件 +│ │ └──cameraManagers +│ │ ├──CamaraManager.ets // 相机会话管理类 +│ │ ├──ImageReceiverManager.ets // ImageReceiver预览流管理类 +│ │ ├──OutputManager.ets // 输出流管理类抽象接口 +│ │ ├──PhotoManager.ets // 拍照流管理类 +│ │ ├──VideoManager.ets // 视频流管理类 +│ │ └──PreviewManager.ets // 预览流管理类 +│ └──Index.ets // 相机模块导出文件 +├──commons/src/main/ets/ +│ └──utils +│ └──Logger.ets // 日志类 +├──entry/src/main/ets/ +│ ├──entryability +│ │ └──EntryAbility.ets // 程序入口类 +│ ├──constants +│ │ └──Constants.ets // 常量文件 +│ ├──pages +│ │ └──Index.ets // 入口预览页面 +│ ├──views +│ │ ├──ModeButtonsView.ets // 拍照模式切换按钮视图 +│ │ ├──OperateButtonsView.ets // 操作按钮视图 +│ │ ├──SettingButtonsView.ets // 设置按钮视图 +│ │ └──ZoomButtonsView.ets // 设置焦距按钮视图 +│ ├──viewModels +│ │ └──PreviewViewModel.ets // 预览相关的状态管理类 +│ └──utils +│ ├──CommonUtil.ets // 通用工具函数模块 +│ ├──PermissionManager.ets // 权限管理类 +│ ├──RefreshableTimer.ets // 定时器管理类 +│ └──WindowUtil.ets // 窗口工具类 +└──entry/src/main/resources // 应用静态资源目录 +``` + +### 具体实现 + +1. 使用Camera Kit相关能力。 +2. 使用`getPreviewRotation`,`getPhotoRotation`,`getVideoRotation`等接口获取预览、拍照和录像的旋转角度,并通过`setPreviewRotation`,`PhotoCaptureSetting.rotation`,`AVMetadata.videoOrientation`等接口(属性)下发预览、拍照和录像的旋转角度。 +3. 在[module.json5](./entry/src/main/module.json5)中配置`orientation`字段为`follow_desktop`,实现应用在直板机和折叠机折叠态是竖屏,在平板和折叠机展开态默认可旋转。详细配置请参考[横竖屏切换](https://developer.huawei.com/consumer/cn/doc/best-practices/bpta-landscape-and-portrait-development)。 +### 相关权限 + +- ohos.permission.CAMERA:用于相机操作 +- ohos.permission.MICROPHONE:麦克风权限,用于录像 +- ohos.permission.MEDIA_LOCATION: 用于获取地理信息 +- ohos.permission.WRITE_IMAGEVIDEO:用于写入媒体文件 +- hos.permission.READ_IMAGEVIDEO:用于读取媒体文件 +- ohos.permission.ACCELEROMETER:用于加速度传感器 + + +### 约束与限制 + +1.本示例仅支持标准系统上运行,支持设备:华为手机、平板。 + +2.HarmonyOS系统:HarmonyOS 5.1.1 Release及以上。 + +3.DevEco Studio版本:DevEco Studio 5.1.1 Release及以上。 + +4.HarmonyOS SDK版本:HarmonyOS 5.1.1 Release SDK及以上。 + diff --git a/CameraKit/RotationDisplayCamera/build-profile.json5 b/CameraKit/RotationDisplayCamera/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..499d1f86a734a1dba1823bb9ddb2f45e2798542e --- /dev/null +++ b/CameraKit/RotationDisplayCamera/build-profile.json5 @@ -0,0 +1,64 @@ +{ + "app": { + "signingConfigs": [ + { + "name": "default", + "type": "HarmonyOS", + "material": { + "certpath": "C:\\Users\\s00888898\\.ohos\\config\\default_CustomCamera-master_-GgiJ_5GmsKiAEgowwsH2ATRm7hr2cKwW7aIeO9VxFs=.cer", + "keyAlias": "debugKey", + "keyPassword": "0000001AD5270856EA4BD0D95BD12F6109ECED245E33915F9B9A4A1753134934372C7C1E93D90CB3B064", + "profile": "C:\\Users\\s00888898\\.ohos\\config\\default_CustomCamera-master_-GgiJ_5GmsKiAEgowwsH2ATRm7hr2cKwW7aIeO9VxFs=.p7b", + "signAlg": "SHA256withECDSA", + "storeFile": "C:\\Users\\s00888898\\.ohos\\config\\default_CustomCamera-master_-GgiJ_5GmsKiAEgowwsH2ATRm7hr2cKwW7aIeO9VxFs=.p12", + "storePassword": "0000001A2F4C7A073BBC5B153BD980D8CBCE010695A6FADD636E3426452DAE497AD9D140D15EBAB321EC" + } + } + ], + "products": [ + { + "name": "default", + "signingConfig": "default", + "targetSdkVersion": "6.0.0(20)", + "compatibleSdkVersion": "6.0.0(20)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + }, + { + "name": "camera", + "srcPath": "./camera" + }, + { + "name": "commons", + "srcPath": "./commons", + } + ] +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/.gitignore b/CameraKit/RotationDisplayCamera/camera/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/BuildProfile.ets b/CameraKit/RotationDisplayCamera/camera/BuildProfile.ets new file mode 100644 index 0000000000000000000000000000000000000000..fca5fff74151f415508b0adcae12fab74bdf8fae --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/BuildProfile.ets @@ -0,0 +1,17 @@ +/** + * Use these variables when you tailor your ArkTS code. They must be of the const type. + */ +export const HAR_VERSION = '1.0.0'; +export const BUILD_MODE_NAME = 'test'; +export const DEBUG = true; +export const TARGET_NAME = 'default'; + +/** + * BuildProfile Class is used only for compatibility purposes. + */ +export default class BuildProfile { + static readonly HAR_VERSION = HAR_VERSION; + static readonly BUILD_MODE_NAME = BUILD_MODE_NAME; + static readonly DEBUG = DEBUG; + static readonly TARGET_NAME = TARGET_NAME; +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/Index.ets b/CameraKit/RotationDisplayCamera/camera/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..ccfbb991d25157b984dd7911b8e1bcb8dffb967b --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/Index.ets @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2025 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. + */ + +export { CameraManager } from './src/main/ets/cameramanagers/CameraManager'; +export { PreviewManager } from './src/main/ets/cameramanagers/PreviewManager'; +export { PhotoManager } from './src/main/ets/cameramanagers/PhotoManager'; +export { VideoManager, AVRecorderState } from './src/main/ets/cameramanagers/VideoManager'; +export { ImageReceiverManager } from './src/main/ets/cameramanagers/ImageReceiverManager'; +export { GridLine } from './src/main/ets/components/GridLine'; +export { LevelIndicator } from './src/main/ets/components/LevelIndicator'; diff --git a/CameraKit/RotationDisplayCamera/camera/build-profile.json5 b/CameraKit/RotationDisplayCamera/camera/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..b7cdea1eacdfeed02f087f59a735d470c6752d09 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/build-profile.json5 @@ -0,0 +1,21 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + + }, + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest" + } + ] +} diff --git a/CameraKit/RotationDisplayCamera/camera/hvigorfile.ts b/CameraKit/RotationDisplayCamera/camera/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..805c5d7f6809c51cff0b4adcc1142979f8f864b6 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/hvigorfile.ts @@ -0,0 +1,6 @@ +import { harTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/oh-package-lock.json5 b/CameraKit/RotationDisplayCamera/camera/oh-package-lock.json5 new file mode 100644 index 0000000000000000000000000000000000000000..ebb8b315c3eb0c6696e7e8e12bd9aaab4c3dd2d2 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/oh-package-lock.json5 @@ -0,0 +1,19 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "commons@../commons": "commons@../commons" + }, + "packages": { + "commons@../commons": { + "name": "commons", + "version": "1.0.0", + "resolved": "../commons", + "registryType": "local" + } + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/oh-package.json5 b/CameraKit/RotationDisplayCamera/camera/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..38b9469f09b3f247b681945414a4278dbdb95b9e --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/oh-package.json5 @@ -0,0 +1,11 @@ +{ + "name": "camera", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "Index.ets", + "author": "", + "license": "Apache-2.0", + "dependencies": { + "commons": "file:../commons" + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/CameraManager.ets b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/CameraManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..55d6b5aac390c70ad19cfc25171f84d8f0e9e132 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/CameraManager.ets @@ -0,0 +1,243 @@ +/* + * Copyright (c) 2025 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 '@kit.CameraKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { display } from '@kit.ArkUI'; +import { Logger } from 'commons'; +import OutputManager, { CreateOutputConfig } from './OutputManager'; +import { PreviewManager } from './PreviewManager'; + +const TAG = 'CameraManager'; + +export class CameraManager { + private cameraManager: camera.CameraManager; + session?: camera.PhotoSession | camera.VideoSession; + private cameraInput?: camera.CameraInput; + private outputManagers: OutputManager[] = []; + + constructor(context: Context, outputManagers: OutputManager[]) { + // [Start cameraManager] + this.cameraManager = camera.getCameraManager(context); + // [End cameraManager] + this.outputManagers = outputManagers; + this.addCameraStatusListener(); + } + + addCameraStatusListener() { + this.cameraManager.on('cameraStatus', (err: BusinessError, statusInfo: camera.CameraStatusInfo) => { + if (err && err.message) { + Logger.error(TAG, 'cameraStatus with errorMessage = ' + err.message); + return; + } + Logger.info(TAG, `cameraStatusInfo: camera is ${statusInfo.camera.cameraId}, status is ${statusInfo.status}`); + }); + } + + getCameraManager() { + return this.cameraManager; + } + + async start( + xComponentSurfaceId: string, + cameraPosition: camera.CameraPosition, + sceneMode: camera.SceneMode, + getProfile: (cameraOrientation: number, picFormat: camera.CameraFormat) => camera.Profile + ) { + try { + const device = this.getCameraDevice(cameraPosition); + if (!device) { + return; + } + // [Start cameraInput] + this.cameraInput = this.cameraManager.createCameraInput(device); + await this.cameraInput.open(); + // [End cameraInput] + // [Start session] + const session = this.cameraManager.createSession(sceneMode); + session.beginConfig(); + session.addInput(this.cameraInput); + // [StartExclude session] + const config: CreateOutputConfig = { + cameraManager: this.cameraManager, + device, + sceneMode, + profile: getProfile(device.cameraOrientation, camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP), + surfaceId: xComponentSurfaceId + }; + // [EndExclude session] + for (const outputManager of this.outputManagers) { + if (outputManager.isActive) { + const output = await outputManager.createOutput(config); + session.addOutput(output); + } + }; + await session.commitConfig(); + for (const outputManager of this.outputManagers) { + if (outputManager.isActive && outputManager instanceof PreviewManager) { + outputManager.setRotate(); + display.off('change'); + display.on('change', () => { + outputManager.setRotate(); + }); + } + }; + await session.start(); + // [End session] + this.session = session as (camera.PhotoSession | camera.VideoSession); + this.setFocusMode(camera.FocusMode.FOCUS_MODE_AUTO); + this.setExposureMode(camera.ExposureMode.EXPOSURE_MODE_AUTO); + } catch (e) { + Logger.error(TAG, `Failed to start camera session. Cause ${JSON.stringify(e)}`); + } + } + + async refreshOutput(oldOutput: camera.CameraOutput, newOutput: camera.CameraOutput) { + await this.session?.stop(); + this.session?.beginConfig(); + this.session?.removeOutput(oldOutput); + this.session?.addOutput(newOutput); + await this.session?.commitConfig(); + await this.session?.start(); + } + + // [Start release] + async release() { + display.off('change'); + await this.session?.stop(); + for (const outputManager of this.outputManagers) { + if (outputManager.isActive) { + await outputManager.release(); + } + }; + await this.cameraInput?.close(); + await this.session?.release(); + } + // [End release] + + // [Start getCameraDevice] + getCameraDevice(cameraPosition: camera.CameraPosition) { + const cameraDevices = this.cameraManager.getSupportedCameras(); + const device = cameraDevices?.find(device => device.cameraPosition === cameraPosition) || cameraDevices[0]; + if (!device) { + Logger.error(TAG, `Failed to get camera device. cameraPosition: ${cameraPosition}}`); + } + return device; + } + // [End getCameraDevice] + + // [Start getZoomRange] + getZoomRange() { + return this.session!.getZoomRatioRange(); + } + // [End getZoomRange] + + // [Start setFocusMode] + setFocusMode(focusMode: camera.FocusMode) { + try { + const isSupported = this.session?.isFocusModeSupported(focusMode); + if (!isSupported) { + Logger.error(TAG, `setFocusMode error: focus mode ${focusMode} is not supported`); + return; + } + this.session?.setFocusMode(focusMode); + } catch(e) { + Logger.error(TAG, 'setFocusMode error ' + JSON.stringify(e)); + } + } + // [End setFocusMode] + + // [Start setFocusPoint] + setFocusPoint(point: camera.Point) { + try { + this.session?.setFocusPoint(point); + } catch(e) { + Logger.error(TAG, 'setFocusPoint error ' + JSON.stringify(e)); + } + } + // [End setFocusPoint] + + // [Start setExposureMode] + setExposureMode(exposureMode: camera.ExposureMode) { + try { + const isSupported = this.session?.isExposureModeSupported(exposureMode); + if (!isSupported) { + Logger.error(TAG, `setExposureMode error: focus mode ${exposureMode} is not supported`); + return; + } + this.session?.setExposureMode(exposureMode); + } catch(e) { + Logger.error(TAG, 'setExposureMode error ' + JSON.stringify(e)); + } + } + // [End setExposureMode] + + // [Start setMeteringPoint] + setMeteringPoint(point: camera.Point) { + try { + this.session?.setMeteringPoint(point); + } catch(e) { + Logger.error(TAG, 'setMeteringPoint error ' + JSON.stringify(e)); + } + } + // [End setMeteringPoint] + + setZoomRatio(zoom: number) { + try { + this.session?.setZoomRatio(zoom); + } catch(e) { + Logger.error(TAG, 'setZoomRatio error ' + JSON.stringify(e)); + } + } + + // [Start setSmoothZoom] + setSmoothZoom(zoom: number) { + try { + this.session?.setSmoothZoom(zoom); + } catch(e) { + Logger.error(TAG, 'setSmoothZoom error ' + JSON.stringify(e)); + } + } + // [End setSmoothZoom] + + // [Start setFlashMode] + setFlashMode(flashMode: camera.FlashMode) { + try { + const isSupported = this.session?.isFlashModeSupported(flashMode); + if (!isSupported) { + Logger.error(TAG, `setFlashMode error: flash mode ${flashMode} is not supported`); + return; + } + this.session?.setFlashMode(flashMode); + } catch(e) { + Logger.error(TAG, 'setFlashMode error ' + JSON.stringify(e)); + } + } + // [End setFlashMode] + + setVideoStabilizationMode(stabilizationMode: camera.VideoStabilizationMode) { + try { + const session = this.session as camera.VideoSession; + const isSupported: boolean = session.isVideoStabilizationModeSupported(stabilizationMode); + if (isSupported) { + session.setVideoStabilizationMode(stabilizationMode); + } else { + Logger.error(TAG, 'stabilizationMode is not supported: ' + JSON.stringify(stabilizationMode)); + } + } catch (e) { + Logger.error(TAG, 'setVideoStabilizationMode error ' + JSON.stringify(e)); + } + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/ImageReceiverManager.ets b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/ImageReceiverManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..83b5a553745cdf4cf164dd0493197b6fbaaf3a48 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/ImageReceiverManager.ets @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2025 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 { image } from '@kit.ImageKit'; +import { camera } from '@kit.CameraKit'; +import { display } from '@kit.ArkUI'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { Logger } from 'commons'; +import OutputManager, { CreateOutputConfig } from './OutputManager'; +import CameraConstant from '../constants/CameraConstants'; + +const TAG = 'ImageReceiverManager'; + +export class ImageReceiverManager implements OutputManager { + output?: camera.PreviewOutput; + isActive: boolean = true; + callback: (px: PixelMap) => void; + private position: camera.CameraPosition = camera.CameraPosition.CAMERA_POSITION_BACK; + + constructor(cb: (px: PixelMap) => void) { + this.callback = cb; + } + + async createOutput(config: CreateOutputConfig) { + const cameraOutputCap = config.cameraManager.getSupportedOutputCapability(config.device, config.sceneMode); + const displayRatio = config.profile.size.width / config.profile.size.height; + const profileWidth = config.profile.size.width; + const previewProfile = cameraOutputCap.previewProfiles + .sort((a, b) => Math.abs(a.size.width - profileWidth) - Math.abs(b.size.width - profileWidth)) + .find(pf => { + const pfDisplayRatio = pf.size.width / pf.size.height; + return pf.format === config.profile.format + && Math.abs(pfDisplayRatio - displayRatio) <= CameraConstant.PROFILE_DIFFERENCE; + }); + if (!previewProfile) { + Logger.error(TAG, 'Failed to get preview profile'); + return; + } + const surfaceId = await this.init(config.profile.size); + this.output = config.cameraManager.createPreviewOutput(previewProfile, surfaceId); + this.position = config.device.cameraPosition; + return this.output; + } + + async release() { + await this.output?.release(); + this.output = undefined; + } + + // [Start init] + async init(size: Size, format = image.ImageFormat.JPEG, capacity = 8) { + const receiver = image.createImageReceiver(size, format, capacity); + const surfaceId = await receiver.getReceivingSurfaceId(); + this.onImageArrival(receiver); + return surfaceId; + } + // [End init] + + // [Start getPixelMap] + async getPixelMap(imgComponent: image.Component, width: number, height: number, stride: number) { + if (stride === width) { + return await image.createPixelMap(imgComponent.byteBuffer, { + size: { height: height, width: width }, + srcPixelFormat: image.PixelMapFormat.NV21, + }); + } + const dstBufferSize = width * height * 1.5; + const dstArr = new Uint8Array(dstBufferSize); + for (let j = 0; j < height * 1.5; j++) { + const srcBuf = new Uint8Array(imgComponent.byteBuffer, j * stride, width); + dstArr.set(srcBuf, j * width); + } + return await image.createPixelMap(dstArr.buffer, { + size: { height: height, width: width }, + srcPixelFormat: image.PixelMapFormat.NV21, + }); + } + // [End getPixelMap] + + // [Start onImageArrival] + onImageArrival(receiver: image.ImageReceiver): void { + receiver.on('imageArrival', () => { + Logger.info(TAG, 'image arrival'); + receiver.readNextImage((err: BusinessError, nextImage: image.Image) => { + if (err || nextImage === undefined) { + Logger.error(TAG, 'readNextImage failed'); + return; + } + // [Start release] + // [Start nextImage] + nextImage.getComponent(image.ComponentType.JPEG, async (err: BusinessError, imgComponent: image.Component) => { + // [StartExclude release] + if (err || imgComponent === undefined) { + Logger.error(TAG, 'getComponent failed'); + } + if (imgComponent.byteBuffer) { + // [StartExclude onImageArrival] + const width = nextImage.size.width; + const height = nextImage.size.height; + const stride = imgComponent.rowStride; + Logger.info(TAG, `getComponent with width:${width} height:${height} stride:${stride}`); + const pixelMap = await this.getPixelMap(imgComponent, width, height, stride); + const displayRotation = display.getDefaultDisplaySync().rotation * camera.ImageRotation.ROTATION_90; + const rotation = this.output!.getPreviewRotation(displayRotation); + if (this.position === camera.CameraPosition.CAMERA_POSITION_FRONT) { + if (displayRotation === 90 || displayRotation === 270) { + await pixelMap.rotate((rotation + 180) % 360); + } else { + await pixelMap.rotate(rotation); + } + await pixelMap.flip(true, false); + } else { + await pixelMap.rotate(rotation); + } + this.callback(pixelMap); + // [EndExclude onImageArrival] + } else { + Logger.error(TAG, 'byteBuffer is null'); + } + // [EndExclude release] + // [StartExclude nextImage] + // [StartExclude onImageArrival] + nextImage.release().then(() => {Logger.info(TAG, 'image release done');}).catch((error: BusinessError) => { + Logger.error(TAG, `Release failed! Code ${error.code},message is ${error.message}`); + }); + Logger.info(TAG, 'image process done'); + // [EndExclude onImageArrival] + // [EndExclude nextImage] + }); + // [End release] + // [End nextImage] + }); + }); + } + // [End onImageArrival] +} diff --git a/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/OutputManager.ets b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/OutputManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..c476ae134b6c41de7df7b61c0cd47eb2700dfd2b --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/OutputManager.ets @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 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 '@kit.CameraKit'; + +export interface CreateOutputConfig { + cameraManager: camera.CameraManager; + device: camera.CameraDevice; + profile: camera.Profile; + sceneMode?: camera.SceneMode; + surfaceId?: string; +} + +// [Start OutputManager] +export default interface OutputManager { + output?: camera.CameraOutput; + isActive: boolean; + createOutput: (config: CreateOutputConfig) => Promise; + release: () => Promise; +} +// [End OutputManager] \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/PhotoManager.ets b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/PhotoManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..88401e6727752b181acee40622200ec2be45b7bd --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/PhotoManager.ets @@ -0,0 +1,396 @@ +/* + * Copyright (c) 2025 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 '@kit.CameraKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { sensor } from '@kit.SensorServiceKit'; +import { Decimal } from '@kit.ArkTS'; +import { image } from '@kit.ImageKit'; +import { colorSpaceManager } from '@kit.ArkGraphics2D'; +import { geoLocationManager } from '@kit.LocationKit'; +import { Logger } from 'commons'; +import OutputManager, { CreateOutputConfig } from './OutputManager'; +import CameraConstant from '../constants/CameraConstants'; + +const TAG_LOG = 'PhotoManager'; + +export class PhotoManager implements OutputManager { + output?: camera.PhotoOutput; + isActive: boolean = true; + context: Context; + isSingle: boolean = false; + location: geoLocationManager.Location | null = null; + private callback: (pixelMap: image.PixelMap, url: string) => void = () => { + }; + + constructor(context: Context, isActive = true, isSingle: boolean) { + this.context = context; + this.isActive = isActive; + this.isSingle = isSingle; + } + + setIsActive(isActive: boolean) { + this.isActive = isActive; + } + + setCallback(callback: (pixelMap: image.PixelMap, url: string) => void) { + this.callback = callback; + } + + async createOutput(config: CreateOutputConfig) { + let cameraPhotoOutput: camera.PhotoOutput | undefined = undefined; + cameraPhotoOutput = this.createPhotoOutput(config.cameraManager, config.device, config.profile); + if (cameraPhotoOutput) { + this.output = cameraPhotoOutput; + this.setPhotoOutputCallback(this.isSingle); + } + return cameraPhotoOutput; + } + + // [Start create_photo_output] + public createPhotoOutput(cameraManager: camera.CameraManager, cameraDevice: camera.CameraDevice, + profile: camera.Profile) { + let cameraPhotoOutput: camera.PhotoOutput | undefined = undefined; + const cameraOutputCapability = + cameraManager.getSupportedOutputCapability(cameraDevice, camera.SceneMode.NORMAL_PHOTO); + let photoProfilesArray: camera.Profile[] | undefined = cameraOutputCapability?.photoProfiles; + if (photoProfilesArray?.length) { + try { + const displayRatio = profile.size.width / profile.size.height; + const profileWidth = profile.size.width; + const PhotoProfile = photoProfilesArray + .sort((a, b) => Math.abs(a.size.width - profileWidth) - Math.abs(b.size.width - profileWidth)) + .find(pf => { + const pfDisplayRatio = pf.size.width / pf.size.height; + return Math.abs(pfDisplayRatio - displayRatio) <= CameraConstant.PROFILE_DIFFERENCE && + pf.format === camera.CameraFormat.CAMERA_FORMAT_JPEG; + }); + if (!PhotoProfile) { + Logger.error(TAG_LOG, 'Failed to get photo profile'); + return; + } + cameraPhotoOutput = cameraManager.createPhotoOutput(PhotoProfile); + } catch (error) { + Logger.error(TAG_LOG, `Failed to createPhotoOutput. error: ${JSON.stringify(error)}`); + } + } + this.output = cameraPhotoOutput; + return cameraPhotoOutput; + } + + // [End create_photo_output] + + // [Start set_photo_cb_double] + async mediaLibSavePhoto(photoAsset: photoAccessHelper.PhotoAsset, + phAccessHelper: photoAccessHelper.PhotoAccessHelper): Promise { + try { + let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest = + new photoAccessHelper.MediaAssetChangeRequest(photoAsset); + assetChangeRequest.saveCameraPhoto(); + await phAccessHelper.applyChanges(assetChangeRequest); + phAccessHelper.release(); + } catch (error) { + Logger.error(TAG_LOG, `apply saveCameraPhoto failed with error: ${error.code}, ${error.message}`); + } + } + + async mediaLibRequestBuffer(photoAsset: photoAccessHelper.PhotoAsset, context: Context, + callback: (pixelMap: image.PixelMap, url: string) => void) { + class MediaDataHandler implements photoAccessHelper.MediaAssetDataHandler { + onDataPrepared(data: ArrayBuffer) { + if (data === undefined) { + Logger.error(TAG_LOG, 'Error occurred when preparing data'); + return; + } + let imageSource = image.createImageSource(data); + imageSource.createPixelMap().then((pixelMap: image.PixelMap) => { + callback(pixelMap, photoAsset.uri); + }).catch((err: BusinessError) => { + Logger.error(TAG_LOG, `createPixelMap err:${err.code}`); + }) + } + } + + let requestOptions: photoAccessHelper.RequestOptions = { + deliveryMode: photoAccessHelper.DeliveryMode.FAST_MODE, + } + const handler = new MediaDataHandler(); + await photoAccessHelper.MediaAssetManager.requestImageData(context, photoAsset, requestOptions, handler); + } + + public setPhotoOutputCbDouble(cameraPhotoOutput: camera.PhotoOutput) { + cameraPhotoOutput.on('photoAssetAvailable', + async (_err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset): Promise => { + let accessHelper: photoAccessHelper.PhotoAccessHelper = + photoAccessHelper.getPhotoAccessHelper(this.context); + this.mediaLibSavePhoto(photoAsset, accessHelper); + this.mediaLibRequestBuffer(photoAsset, this.context, this.callback); + }); + } + + // [End set_photo_cb_double] + + // [Start set_photo_cb_single] + setPhotoOutputCbSingle(photoOutput: camera.PhotoOutput, context: Context) { + photoOutput.on('photoAvailable', (errCode: BusinessError, photo: camera.Photo): void => { + if (errCode || photo === undefined) { + Logger.error(TAG_LOG, 'getPhoto failed'); + return; + } + this.mediaLibSavePhotoSingle(context, photo.main) + }); + } + // [End set_photo_cb_single] + + // [Start save_photo_single] + mediaLibSavePhotoSingle(context: Context, imageObj: image.Image) { + imageObj.getComponent(image.ComponentType.JPEG, async (errCode: BusinessError, component: image.Component) => { + if (errCode || component === undefined) { + Logger.error(TAG_LOG, 'getComponent failed'); + return; + } + const buffer: ArrayBuffer = component.byteBuffer; + if (!buffer) { + Logger.error(TAG_LOG, 'byteBuffer is null'); + return; + } + let photoType: photoAccessHelper.PhotoType = photoAccessHelper.PhotoType.IMAGE; + let extension: string = 'jpg'; + let options: photoAccessHelper.CreateOptions = { + title: 'testPhoto' + } + let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest = + photoAccessHelper.MediaAssetChangeRequest.createAssetRequest(context, photoType, extension, options); + assetChangeRequest.addResource(photoAccessHelper.ResourceType.IMAGE_RESOURCE, buffer) + assetChangeRequest.saveCameraPhoto(); + let accessHelper: photoAccessHelper.PhotoAccessHelper = + photoAccessHelper.getPhotoAccessHelper(context); + await accessHelper.applyChanges(assetChangeRequest); + let imageSource = image.createImageSource(buffer); + let pixelmap = imageSource.createPixelMapSync(); + this.callback(pixelmap, assetChangeRequest.getAsset().uri); + accessHelper.release(); + imageObj.release(); + }); + } + // [End save_photo_single] + + setPhotoOutputCallback(isSingle: boolean) { + if (!this.output) { + return; + } + if (isSingle) { + this.output?.off('photoAssetAvailable'); + this.setPhotoOutputCbSingle(this.output, this.context); + } else { + this.output?.off('photoAvailable'); + this.setPhotoOutputCbDouble(this.output); + } + } + + preparePhoto(session: camera.Session, zoomRatio?: number, flashMode?: camera.FlashMode, + focusMode?: camera.FocusMode) { + const photoSession = session as camera.PhotoSession; + this.setPhotoFlash(photoSession, flashMode); + this.setPhotoFocus(photoSession, focusMode); + this.setPhotoZoomRatio(photoSession, zoomRatio || 0); + } + + // [Start set_color_space] + setColorSpaceBeforeCommitConfig(session: camera.PhotoSession, isHdr: boolean): void { + let colorSpace: colorSpaceManager.ColorSpace = + isHdr ? colorSpaceManager.ColorSpace.DISPLAY_P3 : colorSpaceManager.ColorSpace.SRGB; + let colorSpaces: Array = []; + try { + colorSpaces = session.getSupportedColorSpaces(); + } catch (error) { + Logger.error(TAG_LOG, `The getSupportedColorSpaces call failed. error code: ${error.code}`); + } + if (!colorSpaces.includes(colorSpace)) { + Logger.info(TAG_LOG, `colorSpace: ${colorSpace} is not support`); + return; + } + Logger.info(TAG_LOG, `setColorSpace: ${colorSpace}`); + session.setColorSpace(colorSpace); + try { + let activeColorSpace: colorSpaceManager.ColorSpace = session.getActiveColorSpace(); + Logger.info(TAG_LOG, `activeColorSpace: ${activeColorSpace}`); + } catch (error) { + Logger.error(TAG_LOG, `getActiveColorSpace Faild: ${error.message}`); + } + } + // [End set_color_space] + + public checkFlash(photoSession: camera.PhotoSession) { + let flashModeStatus: boolean = false; + if (photoSession.hasFlash()) { + flashModeStatus = photoSession.isFlashModeSupported(camera.FlashMode.FLASH_MODE_AUTO); + } + return flashModeStatus; + } + + public setPhotoFlash(photoSession: camera.PhotoSession, flashMode?: camera.FlashMode) { + try { + if (this.checkFlash(photoSession)) { + photoSession.setFlashMode(flashMode || camera.FlashMode.FLASH_MODE_CLOSE); + } + } catch (error) { + Logger.error(TAG_LOG, `Failed to hasFlash. error: ${JSON.stringify(error)}`); + } + } + + public setPhotoFocus(photoSession: camera.PhotoSession, focusMode?: camera.FocusMode) { + const defaultMode = camera.FocusMode.FOCUS_MODE_CONTINUOUS_AUTO; + try { + let focusModeStatus: boolean = photoSession.isFocusModeSupported(focusMode || defaultMode); + if (focusModeStatus) { + photoSession.setFocusMode(focusMode || defaultMode); + } + } catch (error) { + Logger.error(TAG_LOG, + `Failed to check whether the focus mode is supported. error: ${JSON.stringify(error)}`); + } + } + + public setFocusPoint(photoSession: camera.PhotoSession, focusPoint: camera.Point): void { + try { + photoSession.setFocusPoint(focusPoint); + } catch (error) { + Logger.error(TAG_LOG, `The setFocusPoint call failed. error code: ${error.code}`); + } + } + + public setPhotoZoomRatio(photoSession: camera.PhotoSession, zoomRatio?: number) { + let photoZoomRatio = 0; + if (!zoomRatio) { + try { + let zoomRatioRange: number[] = photoSession.getZoomRatioRange(); + if (zoomRatioRange?.length) { + photoZoomRatio = zoomRatioRange[0]; + } + } catch (error) { + Logger.error(TAG_LOG, `Failed to get the zoom ratio range. error: ${JSON.stringify(error)}`); + } + } + photoSession.setZoomRatio(zoomRatio || photoZoomRatio); + } + + getSupportedColorSpaces(session: camera.PhotoSession): Array { + let colorSpaces: Array = []; + try { + colorSpaces = session.getSupportedColorSpaces(); + } catch (error) { + Logger.error(TAG_LOG,`The getSupportedColorSpaces call failed. error code: ${error.code}`); + } + return colorSpaces; + } + + // [Start get_photo_rotation] + getPhotoRotation(photoOutput: camera.PhotoOutput, deviceDegree: number): camera.ImageRotation { + let photoRotation: camera.ImageRotation = camera.ImageRotation.ROTATION_0; + try { + photoRotation = photoOutput.getPhotoRotation(deviceDegree); + } catch (error) { + Logger.error(TAG_LOG, `The photoOutput.getPhotoRotation call failed. error code: ${error.code}`); + } + return photoRotation; + } + + // [End get_photo_rotation] + + // [Start capture_photo] + public async capture(isFront: boolean) { + const degree = await this.getPhotoDegree(); + const rotation = this.getPhotoRotation(this.output!, degree); + let settings: camera.PhotoCaptureSetting = { + quality: camera.QualityLevel.QUALITY_LEVEL_HIGH, + rotation, + mirror: isFront + }; + this.output?.capture(settings, (err: BusinessError) => { + if (err) { + Logger.error(TAG_LOG, `Failed to capture the photo. error: ${JSON.stringify(err)}`); + return; + } + Logger.info(TAG_LOG, 'Callback invoked to indicate the photo capture request success.'); + }); + } + // [End capture_photo] + + private calculateDeviceDegree(x: number, y: number, z: number): number { + let deviceDegree: number = 0; + // Determine if the device is approaching a vertical position (perpendicular to the ground) + if ((x * x + y * y) * 3 < z * z) { + return deviceDegree; + } else { + // Calculate the inverse tangent value + let sd: Decimal = Decimal.atan2(y, -x) + // Convert radian values to angle values; + let sc: Decimal = Decimal.round(Number(sd) / Math.PI * 180); + // Adjust angle to be relative to vertical orientation + deviceDegree = 90 - Number(sc); + // Normalize angle to 0-360 degrees range + deviceDegree = deviceDegree >= 0 ? deviceDegree % 360 : deviceDegree % 360 + 360; + } + return deviceDegree; + } + + private getPhotoDegree() { + const promise: Promise = new Promise(resolve => { + sensor.once(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => { + let degree = this.calculateDeviceDegree(data.x, data.y, data.z); + resolve(degree); + }); + }) + return promise; + } + + // [Start is_moving_photo] + public isMovingPhotoSupported(photoOutput: camera.PhotoOutput): boolean { + let isSupported: boolean = false; + try { + isSupported = photoOutput.isMovingPhotoSupported(); + } catch (error) { + Logger.error(TAG_LOG, `The isMovingPhotoSupported call failed. error code: ${error.code}`); + } + return isSupported; + } + + // [End is_moving_photo] + + // [Start enable_moving_photo] + public enableMovingPhoto(enabled: boolean): void { + try { + this.output?.enableMovingPhoto(enabled); + } catch (error) { + Logger.error(TAG_LOG, `The enableMovingPhoto call failed. error code: ${error.code}`); + } + } + + // [End enable_moving_photo] + + // [Start photo_release] + async release() { + await this.output?.release(); + if (this.isSingle) { + this.output?.off('photoAvailable'); + } else { + this.output?.off('photoAssetAvailable'); + } + this.output = undefined; + } + // [End photo_release] +} diff --git a/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/PreviewManager.ets b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/PreviewManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..0c8c5cfc817553ccb1c8be04a7370974e8a80c28 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/PreviewManager.ets @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2025 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 '@kit.CameraKit'; +import { BusinessError } from '@kit.BasicServicesKit'; +import { Logger } from 'commons'; +import { display } from '@kit.ArkUI'; +import OutputManager, { CreateOutputConfig } from './OutputManager'; +import CameraConstant from '../constants/CameraConstants'; + +const TAG_LOG = 'PreviewManager' + +export class PreviewManager implements OutputManager { + output?: camera.PreviewOutput; + isActive: boolean = true; + onPreviewStart: () => void = () => {}; + + constructor(onPreviewStart: () => void) { + this.onPreviewStart = onPreviewStart; + } + + // [Start createOutput] + async createOutput(config: CreateOutputConfig) { + const cameraOutputCap = config.cameraManager.getSupportedOutputCapability(config.device, config.sceneMode); + const displayRatio = config.profile.size.width / config.profile.size.height; + const profileWidth = config.profile.size.width; + const previewProfile = cameraOutputCap.previewProfiles + .sort((a, b) => Math.abs(a.size.width - profileWidth) - Math.abs(b.size.width - profileWidth)) + .find(pf => { + const pfDisplayRatio = pf.size.width / pf.size.height; + return pf.format === config.profile.format + && Math.abs(pfDisplayRatio - displayRatio) <= CameraConstant.PROFILE_DIFFERENCE; + }); + if (!previewProfile) { + Logger.error(TAG_LOG, 'Failed to get preview profile'); + return; + } + this.output = config.cameraManager.createPreviewOutput(previewProfile, config.surfaceId); + this.addOutputListener(this.output); + return this.output; + } + // [End createOutput] + + addOutputListener(output: camera.PreviewOutput) { + this.addFrameStartEventListener(output); + this.addFrameEndEventListener(output); + } + + // [Start setRotate] + setRotate(){ + try { + Logger.info(TAG_LOG,`setRotate enter`); + let displayRotation = display.getDefaultDisplaySync().rotation; + let imageRotation = displayRotation * camera.ImageRotation.ROTATION_90; + let previewRotation = this.output?.getPreviewRotation(imageRotation); + let isDisplayLocked: boolean = true;// 建议与setXComponentSurfaceRotation入参的lock属性保持一致 + this.output?.setPreviewRotation(previewRotation, isDisplayLocked); + } catch (error) { + let err = error as BusinessError; + Logger.info(TAG_LOG,`setRotate call failed. error code: ${err.code}`); + } + } + // [End setRotate] + + // [Start onFrame] + addFrameStartEventListener(output: camera.PreviewOutput) { + output.on('frameStart', (err: BusinessError) => { + if (err !== undefined && err.code !== 0) { + Logger.error(TAG_LOG, `FrameStart callback Error, errorMessage: ${err.message}`); + return; + } + Logger.info(TAG_LOG, 'Preview frame started'); + this.onPreviewStart(); + }); + } + + addFrameEndEventListener(output: camera.PreviewOutput) { + output.on('frameEnd', (err: BusinessError) => { + if (err !== undefined && err.code !== 0) { + Logger.error(TAG_LOG, `FrameStart callback Error, errorMessage: ${err.message}`); + return; + } + Logger.info(TAG_LOG, 'Preview frame end'); + }); + } + // [End onFrame] + + // [Start release] + async release() { + await this.output?.release(); + this.output = undefined; + } + // [End release] + + // [Start getSupportedFrameRates] + getSupportedFrameRates() { + return this.output?.getSupportedFrameRates(); + } + // [End getSupportedFrameRates] + + // [Start setFrameRate] + setFrameRate(minFps: number, maxFps: number) { + try { + this.output?.setFrameRate(minFps, maxFps); + } catch(e) { + Logger.error(TAG_LOG, 'setFrameRate error ' + JSON.stringify(e)); + } + } + // [End setFrameRate] +} diff --git a/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/VideoManager.ets b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/VideoManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..c6d3794c75c8ff47210781ed7c361c29b863cc47 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/src/main/ets/cameramanagers/VideoManager.ets @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2025 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 '@kit.MediaKit'; +import { camera } from '@kit.CameraKit'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import { fileIo } from '@kit.CoreFileKit'; +import { sensor } from '@kit.SensorServiceKit'; +import { Decimal } from '@kit.ArkTS'; +import { image } from '@kit.ImageKit'; +import { colorSpaceManager } from '@kit.ArkGraphics2D'; +import { Logger } from 'commons'; +import OutputManager, { CreateOutputConfig } from './OutputManager'; +import CameraConstant from '../constants/CameraConstants'; + +const TAG_LOG = 'video'; + +enum QualityLevel { + NORMAL, + HIGHER +} + +export enum AVRecorderState { + IDLE = 'idle', + PREPARED = 'prepared', + STARTED = 'started', + PAUSED = 'paused', + STOPPED = 'stopped', + RELEASED = 'released', + ERROR = 'error' +} + +export class VideoManager implements OutputManager { + private avRecorder: media.AVRecorder | undefined = undefined; + private avConfig: media.AVRecorderConfig | undefined = undefined; + private avProfile: media.AVRecorderProfile | undefined = undefined; + private videoProfile: camera.VideoProfile | undefined = undefined; + private context: Context | undefined = undefined; + private cameraPosition: number = 0; + private qualityLevel: QualityLevel = QualityLevel.NORMAL; + output: camera.VideoOutput | undefined = undefined; + private videoUri: string = ''; + private file: fileIo.File | undefined = undefined; + state: media.AVRecorderState = AVRecorderState.IDLE; + isActive: boolean = false; + private callback: (pixelMap: image.PixelMap, url: string) => void = () => { + }; + + constructor(context: Context) { + this.context = context; + } + + setIsActive(isActive: boolean) { + this.isActive = isActive; + } + + async createOutput(config: CreateOutputConfig) { + try { + this.avRecorder = await media.createAVRecorder(); + this.avRecorder.on('stateChange', state => { + this.state = state; + Logger.info(TAG_LOG, 'on avRecorder state change: ', state) + }); + } catch (error) { + Logger.info(TAG_LOG, 'createAVRecorder call failed. error code: %{public}s', error.code); + } + if (this.avRecorder === undefined || this.avRecorder === null) { + return; + } + this.setVideoProfile(config.cameraManager, config.profile, config.device); + // this.setVideoProfile(config.cameraManager, config.profile, config.device); + await this.setAVConfig(); + await this.prepare(); + await this.createVideoOutput(config.cameraManager); + return this.output; + } + + async prepare() { + try { + if (this.avRecorder?.state === AVRecorderState.IDLE && this.avConfig) { + await this.avRecorder.prepare(this.avConfig); + Logger.info(TAG_LOG, 'Succeeded in preparing'); + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to prepare and catch error is ${error.message}`); + } + } + + isSupportMirror() { + let isSupported: boolean | undefined = this.output?.isMirrorSupported(); + return isSupported; + } + + // [Start start_video] + async start(isFront: boolean) { + try { + if (this.avRecorder?.state === AVRecorderState.PREPARED) { + if (this.isSupportMirror() && isFront) { + this.output?.enableMirror(true) + } + // [StartExclude start_video] + await this.avRecorder.updateRotation(this.getVideoRotation(await this.getGravity())); + // [EndExclude start_video] + await this.output?.start(); + await this.avRecorder?.start(); + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to start and catch error is ${error.message}`); + } + } + // [End start_video] + + // [Start stop_video] + async stop() { + try { + if (this.avRecorder?.state === AVRecorderState.STARTED + || this.avRecorder?.state === AVRecorderState.PAUSED) { + await this.avRecorder.stop(); + await this.output?.stop(); + const thumbnail = await this.getVideoThumbnail(); + if (thumbnail) { + this.callback(thumbnail, this.videoUri); + } + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to stop and catch error is ${error.message}`); + } + } + // [End stop_video] + + // [Start pause_video] + async pause() { + try { + if (this.avRecorder?.state === AVRecorderState.STARTED) { + await this.avRecorder.pause(); + await this.output?.stop(); + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to pause and catch error is ${error.message}`); + } + } + // [End pause_video] + + // [Start resume_video] + async resume() { + try { + if (this.avRecorder?.state === AVRecorderState.PAUSED) { + await this.output?.start(); + await this.avRecorder.resume(); + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to resume and catch error is ${error.message}`); + } + } + // [End resume_video] + + // [Start release_video] + async release() { + await this.avRecorder?.release(); + await this.output?.release(); + this.file && await fileIo.close(this.file.fd); + this.avRecorder?.off('stateChange'); + this.avRecorder = undefined; + this.output = undefined; + this.file = undefined; + } + // [End release_video] + + getCurrentOutput() { + return this.output; + } + + setVideoCallback(callback: (pixelMap: image.PixelMap, url: string) => void) { + this.callback = callback; + } + + // [Start create_video_output] + async createVideoOutput(cameraManager: camera.CameraManager) { + if (!this.avRecorder || this.avRecorder.state !== AVRecorderState.PREPARED) { + return; + } + try { + // [Start get_surface_id] + let videoSurfaceId = await this.avRecorder.getInputSurface(); + // [End get_surface_id] + this.output = cameraManager.createVideoOutput(this.videoProfile, videoSurfaceId); + } catch (error) { + Logger.error(TAG_LOG, + `Failed to create the output instance. error code: ${error.code}`); + } + } + + setVideoProfile(cameraManager: camera.CameraManager, targetProfile: camera.Profile, + device: camera.CameraDevice) { + this.cameraPosition = device.cameraPosition; + let cameraOutputCap: camera.CameraOutputCapability | undefined = + cameraManager.getSupportedOutputCapability(device, + camera.SceneMode.NORMAL_VIDEO); + let videoProfilesArray: camera.VideoProfile[] | undefined = cameraOutputCap?.videoProfiles; + if (videoProfilesArray?.length) { + try { + const displayRatio = targetProfile.size.width / targetProfile.size.height; + const profileWidth = targetProfile.size.width; + const videoProfile = videoProfilesArray + .sort((a, b) => Math.abs(a.size.width - profileWidth) - Math.abs(b.size.width - profileWidth)) + .find(pf => { + const pfDisplayRatio = pf.size.width / pf.size.height; + return Math.abs(pfDisplayRatio - displayRatio) <= CameraConstant.PROFILE_DIFFERENCE && + pf.format === camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP; + }); + if (!videoProfile) { + Logger.error(TAG_LOG, 'Failed to get video profile'); + return; + } + this.videoProfile = videoProfile; + } catch (error) { + Logger.error(TAG_LOG, `Failed to createPhotoOutput. error: ${JSON.stringify(error)}`); + } + } + } + // [End create_video_output] + + getCameraImageRotation(): camera.ImageRotation { + return this.cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT + ? camera.ImageRotation.ROTATION_270 + : camera.ImageRotation.ROTATION_90 + } + + async setAVConfig() { + // [Start create_file] + let options: photoAccessHelper.CreateOptions = { + title: Date.now().toString() + }; + let accessHelper: photoAccessHelper.PhotoAccessHelper = photoAccessHelper.getPhotoAccessHelper(this.context); + this.videoUri = await accessHelper.createAsset(photoAccessHelper.PhotoType.VIDEO, 'mp4', options); + this.file = fileIo.openSync(this.videoUri, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE); + // [End create_file] + + // [Start av_profile] + this.avProfile = { + audioBitrate: 48000, + audioChannels: 2, + audioCodec: media.CodecMimeType.AUDIO_AAC, + audioSampleRate: 48000, + fileFormat: media.ContainerFormatType.CFT_MPEG_4, + videoBitrate: 32000000, + videoCodec: (this.qualityLevel === QualityLevel.HIGHER && this.cameraPosition === 0) ? + media.CodecMimeType.VIDEO_HEVC : media.CodecMimeType.VIDEO_AVC, + videoFrameWidth: this.videoProfile?.size.width, + videoFrameHeight: this.videoProfile?.size.height, + videoFrameRate: this.cameraPosition === 0 ? 60 : 30, + } + // [End av_profile] + + // [Start av_config] + this.avConfig = { + audioSourceType: media.AudioSourceType.AUDIO_SOURCE_TYPE_CAMCORDER, + videoSourceType: media.VideoSourceType.VIDEO_SOURCE_TYPE_SURFACE_YUV, + profile: this.avProfile, + url: `fd://${this.file.fd}`, + metadata: { + videoOrientation: this.getCameraImageRotation().toString() + } + } + // [End av_config] + } + + getRealData(data: sensor.GravityResponse): number { + let getDeviceDegree: number = 0; + let x = data.x; + let y = data.y; + let z = data.z; + // Determine if the device is approaching a vertical position (perpendicular to the ground) + if ((x * x + y * y) * 3 < z * z) { + return getDeviceDegree; + } else { + // Calculate the inverse tangent value + let sd: Decimal = Decimal.atan2(y, -x); + // Convert radian values to angle values; + let sc: Decimal = Decimal.round(Number(sd) / 3.141592653589 * 180) + // Adjust angle to be relative to vertical orientation + getDeviceDegree = 90 - Number(sc); + // Normalize angle to 0-360 degrees range + getDeviceDegree = getDeviceDegree >= 0 ? getDeviceDegree % 360 : getDeviceDegree % 360 + 360; + } + return getDeviceDegree; + } + + async getGravity(): Promise { + try { + let isSupportedGravity: boolean = false; + let data = await sensor.getSensorList(); + for (let i = 0; i < data.length; i++) { + if (data[i].sensorId === sensor.SensorId.GRAVITY) { + isSupportedGravity = true; + break; + } + } + if (isSupportedGravity === true) { + const promise: Promise = new Promise((resolve) => { + sensor.once(sensor.SensorId.GRAVITY, (data: sensor.GravityResponse) => { + resolve(this.getRealData(data)); + }); + }) + return promise; + } else { + const promise: Promise = new Promise((resolve) => { + sensor.once(sensor.SensorId.ACCELEROMETER, (data: sensor.AccelerometerResponse) => { + resolve(this.getRealData(data as sensor.GravityResponse)); + }); + }) + return promise; + } + } catch (error) { + Logger.info(TAG_LOG, `Failed to getGravity and catch error is ${error.message}`); + return 0 + } + } + + // [Start get_video_rotation] + getVideoRotation(deviceDegree: number): camera.ImageRotation { + let videoRotation: camera.ImageRotation = this.getCameraImageRotation(); + try { + videoRotation = this.output!.getVideoRotation(deviceDegree); + Logger.info(TAG_LOG, `Video rotation is: ${videoRotation}`); + } catch (error) { + Logger.info(TAG_LOG, `Failed to getVideoRotation and catch error is: ${error.message}`); + } + return videoRotation; + } + // [End get_video_rotation] + + async getVideoThumbnail() { + let pixelMap: image.PixelMap | undefined = undefined; + try { + let avImageGenerator: media.AVImageGenerator = await media.createAVImageGenerator(); + let dataSrc: media.AVFileDescriptor = { + fd: this.file!.fd, + }; + avImageGenerator.fdSrc = dataSrc; + let timeUs = 0; + let queryOption = media.AVImageQueryOptions.AV_IMAGE_QUERY_NEXT_SYNC; + let param: media.PixelMapParams = { + width: 300, + height: 300 + }; + pixelMap = await avImageGenerator.fetchFrameByTime(timeUs, queryOption, param); + avImageGenerator.release(); + } catch (error) { + Logger.info(TAG_LOG, `Failed to getVideoThumbnail and catch error is ${error.message}`); + } + return pixelMap; + } + + isRecording() { + return this.state === AVRecorderState.STARTED || this.state === AVRecorderState.PAUSED; + } + + + // [Start set_video_stabilization] + setVideoStabilizationMode(session: camera.VideoSession): boolean { + let mode: camera.VideoStabilizationMode = camera.VideoStabilizationMode.AUTO; + // Check whether video stabilization is supported + let isSupported: boolean = session.isVideoStabilizationModeSupported(mode); + if (!isSupported) { + Logger.info(TAG_LOG, `videoStabilizationMode: ${mode} is not support`); + return false; + } + Logger.info(TAG_LOG, `setVideoStabilizationMode: ${mode}`); + // Set video stabilization + session.setVideoStabilizationMode(mode); + let activeVideoStabilizationMode = session.getActiveVideoStabilizationMode(); + Logger.info(TAG_LOG, `activeVideoStabilizationMode: ${activeVideoStabilizationMode}`); + return isSupported; + } + // [End set_video_stabilization] + + // [Start set_video_color_space] + getSupportedColorSpaces(session: camera.VideoSession): Array { + let colorSpaces: colorSpaceManager.ColorSpace[] = []; + try { + colorSpaces = session.getSupportedColorSpaces(); + } catch (error) { + Logger.error(TAG_LOG, `The getSupportedColorSpaces call failed. error code: ${error.message}`); + } + return colorSpaces; + } + + setColorSpaceAfterCommitConfig(session: camera.VideoSession, isHdr: boolean): void { + let colorSpace: colorSpaceManager.ColorSpace = + isHdr ? colorSpaceManager.ColorSpace.BT2020_HLG_LIMIT : colorSpaceManager.ColorSpace.BT709_LIMIT; + let colorSpaces: colorSpaceManager.ColorSpace[] = this.getSupportedColorSpaces(session); + if (!colorSpaces.includes(colorSpace)) { + Logger.info(TAG_LOG, `colorSpace: ${colorSpace} is not support`); + return; + } + Logger.info(TAG_LOG, `setColorSpace: ${colorSpace}`); + session.setColorSpace(colorSpace); + try { + let activeColorSpace: colorSpaceManager.ColorSpace = session.getActiveColorSpace(); + Logger.info(TAG_LOG, `activeColorSpace: ${activeColorSpace}`); + } catch (error) { + Logger.error(TAG_LOG, `getActiveColorSpace Faild: ${error.message}`); + } + } + // [Start set_video_color_space] +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/src/main/ets/components/GridLine.ets b/CameraKit/RotationDisplayCamera/camera/src/main/ets/components/GridLine.ets new file mode 100644 index 0000000000000000000000000000000000000000..dfdc02e402cdfe4a53766510db66ed23018d57cd --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/src/main/ets/components/GridLine.ets @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 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. + */ + +@Component +export struct GridLine { + private settings: RenderingContextSettings = new RenderingContextSettings(true); + private context: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings); + @Prop cols: number = 3; + @Prop rows: number = 3; + @Prop strokeStyle: string |number |CanvasGradient | CanvasPattern = Color.White; + @Prop lineWidth: number = 1; + + // [Start draw] + draw() { + const ctx = this.context; + ctx.strokeStyle = this.strokeStyle; + ctx.lineWidth = this.lineWidth; + const height = this.context.height; + const width = this.context.width; + // horizontal + for (let i = 1; i < this.cols; i++) { + const x = (width / this.cols) * i; + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + ctx.stroke(); + } + // vertical + for (let i = 1; i < this.rows; i++) { + const y = (height / this.rows) * i; + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(width, y); + ctx.stroke(); + } + } + + build() { + Canvas(this.context) + .width('100%') + .height('100%') + .hitTestBehavior(HitTestMode.Transparent) + .onReady(() => this.draw()) + } + // [End draw] +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/src/main/ets/components/LevelIndicator.ets b/CameraKit/RotationDisplayCamera/camera/src/main/ets/components/LevelIndicator.ets new file mode 100644 index 0000000000000000000000000000000000000000..b3e6ac57d1c83d978e9842ad459dec00dfaf6791 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/src/main/ets/components/LevelIndicator.ets @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 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 { curves, display } from '@kit.ArkUI'; +import { sensor } from '@kit.SensorServiceKit'; + +const ANGLE_DIFFERENCE: number = 3; + +// [Start LevelIndicator] +@Component +export struct LevelIndicator { + @Prop acc: sensor.AccelerometerResponse; + + getRotate() { + const rotation = display.getDefaultDisplaySync().rotation * 90; + if (rotation === 90 || rotation === 270) { + return -Math.atan2(-this.acc.y, this.acc.x) * (180 / Math.PI); + } + return -Math.atan2(-this.acc.x, this.acc.y) * (180 / Math.PI); + } + + isAlign() { + return Math.abs(this.getRotate()) - 0 <= ANGLE_DIFFERENCE + || Math.abs(Math.abs(this.getRotate()) - 90) <= ANGLE_DIFFERENCE; + } + + build() { + Stack({ alignContent: Alignment.Center }) { + Line({ + width: 200, + height: 1 + }) + // [StartExclude LevelIndicator] + .stroke(Color.White) + .endPoint([200, 0]) + // [EndExclude LevelIndicator] + .strokeDashArray([3, this.isAlign() ? 0 : 3]) + .opacity(this.isAlign() ? 1 : 0.5) + .rotate({ angle: this.getRotate(), centerX: '50%', centerY: '50%' }) + .animation({ + curve: curves.springMotion(0.6, 0.8), + iterations: 1, + playMode: PlayMode.Normal + }) + Circle() + // [StartExclude LevelIndicator] + .width(48) + .height(48) + .stroke(Color.White) + .fill(Color.Transparent) + // [EndExclude LevelIndicator] + .opacity(this.isAlign() ? 1 : 0.5) + } + // [StartExclude LevelIndicator] + .width('100%') + .height('100%') + // [EndExclude LevelIndicator] + .hitTestBehavior(HitTestMode.Transparent) + } +} +// [End LevelIndicator] diff --git a/CameraKit/RotationDisplayCamera/camera/src/main/ets/constants/CameraConstants.ets b/CameraKit/RotationDisplayCamera/camera/src/main/ets/constants/CameraConstants.ets new file mode 100644 index 0000000000000000000000000000000000000000..780f111cdc5b073fe0ef0f4c987da03823b69f21 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/src/main/ets/constants/CameraConstants.ets @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2025 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. + */ + +class CameraConstant { + static readonly PROFILE_DIFFERENCE = 1e-10; +} + +export default CameraConstant; \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/camera/src/main/module.json5 b/CameraKit/RotationDisplayCamera/camera/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..c5409dfaf8befd98d4e0d6076a3d487d2c7e565e --- /dev/null +++ b/CameraKit/RotationDisplayCamera/camera/src/main/module.json5 @@ -0,0 +1,10 @@ +{ + "module": { + "name": "camera", + "type": "har", + "deviceTypes": [ + "default", + "tablet" + ] + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/commons/.gitignore b/CameraKit/RotationDisplayCamera/commons/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/commons/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/commons/BuildProfile.ets b/CameraKit/RotationDisplayCamera/commons/BuildProfile.ets new file mode 100644 index 0000000000000000000000000000000000000000..fca5fff74151f415508b0adcae12fab74bdf8fae --- /dev/null +++ b/CameraKit/RotationDisplayCamera/commons/BuildProfile.ets @@ -0,0 +1,17 @@ +/** + * Use these variables when you tailor your ArkTS code. They must be of the const type. + */ +export const HAR_VERSION = '1.0.0'; +export const BUILD_MODE_NAME = 'test'; +export const DEBUG = true; +export const TARGET_NAME = 'default'; + +/** + * BuildProfile Class is used only for compatibility purposes. + */ +export default class BuildProfile { + static readonly HAR_VERSION = HAR_VERSION; + static readonly BUILD_MODE_NAME = BUILD_MODE_NAME; + static readonly DEBUG = DEBUG; + static readonly TARGET_NAME = TARGET_NAME; +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/commons/Index.ets b/CameraKit/RotationDisplayCamera/commons/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..621cafbe0df5e8ab8fb2fe0240a9ecc6378d2dc5 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/commons/Index.ets @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2025 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. + */ + +export { Logger } from './src/main/ets/utils/Logger'; diff --git a/CameraKit/RotationDisplayCamera/commons/build-profile.json5 b/CameraKit/RotationDisplayCamera/commons/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..284a1c3355b0643262a13c0b842eb667a433e2d3 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/commons/build-profile.json5 @@ -0,0 +1,20 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + }, + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest" + } + ] +} diff --git a/CameraKit/RotationDisplayCamera/commons/hvigorfile.ts b/CameraKit/RotationDisplayCamera/commons/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..805c5d7f6809c51cff0b4adcc1142979f8f864b6 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/commons/hvigorfile.ts @@ -0,0 +1,6 @@ +import { harTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: harTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins: [] /* Custom plugin to extend the functionality of Hvigor. */ +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/commons/oh-package.json5 b/CameraKit/RotationDisplayCamera/commons/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..d50184a32b1cf153f7dc71afeb97d601086fa6a7 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/commons/oh-package.json5 @@ -0,0 +1,9 @@ +{ + "name": "commons", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "Index.ets", + "author": "", + "license": "Apache-2.0", + "dependencies": {} +} diff --git a/CameraKit/RotationDisplayCamera/commons/src/main/ets/utils/Logger.ets b/CameraKit/RotationDisplayCamera/commons/src/main/ets/utils/Logger.ets new file mode 100644 index 0000000000000000000000000000000000000000..c769a6076e62e5ec82a07d80992354b31e0b5702 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/commons/src/main/ets/utils/Logger.ets @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 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 { hilog } from '@kit.PerformanceAnalysisKit'; + +export class Logger { + private static readonly DOMAIN: number = 0xFF00; + private static readonly TAG: string = 'com.example.customcamera'; + private static readonly PREFIX: string = '[camera-log]'; + + public static debug(logTag: string, messageFormat: string, ...args: Object[]): void { + hilog.debug(Logger.DOMAIN, Logger.TAG, `${Logger.PREFIX} ${logTag}: ${messageFormat}`, args); + } + + public static info(logTag: string, messageFormat: string, ...args: Object[]): void { + hilog.info(Logger.DOMAIN, Logger.TAG, `${Logger.PREFIX} ${logTag}: ${messageFormat}`, args); + } + + public static warn(logTag: string, messageFormat: string, ...args: Object[]): void { + hilog.warn(Logger.DOMAIN, Logger.TAG, `${Logger.PREFIX} ${logTag}: ${messageFormat}`, args); + } + + public static error(logTag: string, messageFormat: string, ...args: Object[]): void { + hilog.error(Logger.DOMAIN, Logger.TAG, `${Logger.PREFIX} ${logTag}: ${messageFormat}`, args); + } + + public static fatal(logTag: string, messageFormat: string, ...args: Object[]): void { + hilog.fatal(Logger.DOMAIN, Logger.TAG, `${Logger.PREFIX} ${logTag}: ${messageFormat}`, args); + } + + private constructor() { + } +} diff --git a/CameraKit/RotationDisplayCamera/commons/src/main/module.json5 b/CameraKit/RotationDisplayCamera/commons/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..6bae1d9e86d12d221ca82d741d95b8e9e59eb1f1 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/commons/src/main/module.json5 @@ -0,0 +1,10 @@ +{ + "module": { + "name": "commons", + "type": "har", + "deviceTypes": [ + "default", + "tablet" + ] + } +} diff --git a/CameraKit/RotationDisplayCamera/entry/.gitignore b/CameraKit/RotationDisplayCamera/entry/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/build-profile.json5 b/CameraKit/RotationDisplayCamera/entry/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..50e33db6ec3d25c19652a894127becfb3761a01b --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/build-profile.json5 @@ -0,0 +1,26 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/hvigorfile.ts b/CameraKit/RotationDisplayCamera/entry/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6edcd90486dd5a853cf7d34c8647f08414ca7a3 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/CameraKit/RotationDisplayCamera/entry/oh-package-lock.json5 b/CameraKit/RotationDisplayCamera/entry/oh-package-lock.json5 new file mode 100644 index 0000000000000000000000000000000000000000..01723a1209faf07a9d88680f47976b49b776a0e9 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/oh-package-lock.json5 @@ -0,0 +1,29 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "camera@../camera": "camera@../camera", + "commons@../commons": "commons@../commons" + }, + "packages": { + "camera@../camera": { + "name": "camera", + "version": "1.0.0", + "resolved": "../camera", + "registryType": "local", + "dependencies": { + "commons": "file:../commons" + } + }, + "commons@../commons": { + "name": "commons", + "version": "1.0.0", + "resolved": "../commons", + "registryType": "local" + } + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/oh-package.json5 b/CameraKit/RotationDisplayCamera/entry/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..4f1c83bce2a51958343ae478f42843ac49efba07 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/oh-package.json5 @@ -0,0 +1,13 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": { + "commons": "file:../commons", + "camera": "file:../camera" + } +} + diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/constants/Constants.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/constants/Constants.ets new file mode 100644 index 0000000000000000000000000000000000000000..63a319b6d0c08a541568b7e723e8fd367785b0b7 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/constants/Constants.ets @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 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 { Permissions } from "@kit.AbilityKit"; + +class CameraConstant { + static readonly RATIO_PHOTO: number = 4 / 3; + static readonly RATIO_VIDEO: number = 16 / 9; + static readonly PERMISSIONS: Permissions[] = [ + 'ohos.permission.CAMERA', + 'ohos.permission.MICROPHONE', + 'ohos.permission.MEDIA_LOCATION', + 'ohos.permission.WRITE_IMAGEVIDEO', + 'ohos.permission.READ_IMAGEVIDEO', + 'ohos.permission.APPROXIMATELY_LOCATION' + ]; +} + +export default CameraConstant; \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/entryability/EntryAbility.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..fe3138ba2df7682844b5eba1433f0a721afde588 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 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 { ConfigurationConstant, UIAbility } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; +import WindowUtil from '../utils/WindowUtil'; + +const DOMAIN = 0x0000; + +export default class EntryAbility extends UIAbility { + onCreate(): void { + this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET); + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy(): void { + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate'); + + WindowUtil.enterImmersive(windowStage.getMainWindowSync()); + + windowStage.loadContent('pages/Index', (err) => { + if (err.code) { + hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err)); + return; + } + hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.'); + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground'); + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/pages/Index.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..1f74880c484a8c242e1c0df26b603eab4edf9a0f --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,523 @@ +/* + * Copyright (c) 2025 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 { sensor } from '@kit.SensorServiceKit'; +import { common } from '@kit.AbilityKit'; +import { display } from '@kit.ArkUI'; +import { curves } from '@kit.ArkUI'; +import { + CameraManager, + GridLine, + ImageReceiverManager, + LevelIndicator, + PhotoManager, + PreviewManager, + VideoManager +} from 'camera'; +import CameraConstant from '../constants/Constants'; +import { calCameraPoint, getClampedChildPosition, limitNumberInRange, showToast } from '../utils/CommonUtil'; +import RefreshableTimer from '../utils/RefreshableTimer'; +import PermissionManager from '../utils/PermissionManager'; +import ZoomButtonsView from '../views/ZoomButtonsView'; +import ModeButtonsView from '../views/ModeButtonsView'; +import SettingButtonsView from '../views/SettingButtonsView'; +import OperateButtonsView from '../views/OperateButtonsView'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; + +@Extend(SymbolGlyph) +function funcButtonStyle() { + .fontSize(22) + .fontColor([Color.White]) + .borderRadius('50%') + .padding(12) + .backgroundColor('#664D4D4D') +} + +@Entry +@Component +struct Index { + private context: Context = this.getUIContext().getHostContext()!; + private applicationContext = this.context.getApplicationContext(); + private windowClass = (this.context as common.UIAbilityContext).windowStage.getMainWindowSync(); + + @State videoManager: VideoManager = new VideoManager(this.context); + @State isSinglePhoto: boolean = false; + @State isLivePhoto: boolean = false; + private photoManager: PhotoManager = new PhotoManager(this.context, true, this.isSinglePhoto); + private previewManager: PreviewManager = new PreviewManager(() => { this.onPreviewStart() }); + private imageReceiverManager: ImageReceiverManager = new ImageReceiverManager(px => { + this.onImageReceiver(px); + }); + private cameraManager: CameraManager = new CameraManager(this.context, [this.previewManager, + this.photoManager, this.videoManager, this.imageReceiverManager]); + + @State previewVM: PreviewViewModel = new PreviewViewModel(this.getUIContext()); + + @State isGridLineVisible: boolean = false; + @State isLevelIndicatorVisible: boolean = false; + @State isPreviewImageVisible: boolean = false; + + @State isFocusBoxVisible: boolean = false; + @State focusBoxPosition: Edges = { top: 0, left: 0 }; + private focusBoxSize: Size = { width: 80, height: 80 }; + private focusBoxTimer: RefreshableTimer = new RefreshableTimer(() => { + this.isFocusBoxVisible = false; + }, 3 * 1000); + private exposureFontSize: number = 24; + + @State isSleeping: boolean = false; + private sleepTimer?: RefreshableTimer; + + private zoomRange: number[] = []; + @State zooms: number[] = [1, 5, 10]; + @State currentZoom: number = 1; + @State isZoomPinching: boolean = false; + private originZoomBeforePinch: number = 1; // record zoom after pinch, sale base it. + + @State previewImage: PixelMap | ResourceStr = ''; + private PreviewImageHeight: number = 80; + + @State photoDelayTime: number = 0; + @State photoRemainder: number = 0; + @State isDelayTakePhoto:boolean = false; + + @State acc: sensor.AccelerometerResponse = { x: 0, y: 0, z: 0 } as sensor.AccelerometerResponse; + private setPreviewSize: () => void = () => { this.previewVM.setPreviewSize(); } + + @State isShowBlack: boolean = false; + @StorageLink('captureClick') @Watch('onCaptureClick') captureClickFlag: number = 0; + @State flashBlackOpacity: number = 1; + + async aboutToAppear() { + this.addGravityEventListener(); + this.initSleepTimer(); + this.registerApplicationStateChange(); + this.addOrientationChangeEventListener(); + display.on('foldStatusChange', () => { this.onFoldStatusChange() }); + } + + aboutToDisappear(): void { + this.removeOrientationChangeEventListener(); + } + + // [Start addGravityEventListener] + addGravityEventListener() { + sensor.on(sensor.SensorId.GRAVITY, (data) => { + this.acc = data; + }, { interval: 100 * 1000 * 1000 }); // 100ms + } + // [End addGravityEventListener] + + addOrientationChangeEventListener() { + this.windowClass.on('windowSizeChange', this.setPreviewSize); + } + + removeOrientationChangeEventListener() { + this.windowClass.off('windowSizeChange', this.setPreviewSize); + } + + onImageReceiver(pixelMap: PixelMap) { + this.previewImage = pixelMap; + } + + // [Start initSleepTimer] + initSleepTimer() { + this.sleepTimer = new RefreshableTimer(() => { + this.previewVM.openPreviewBlur(); + this.isSleeping = true; + this.cameraManager.release(); + }, 30 * 1000); + this.sleepTimer.start(); + const observer = this.getUIContext().getUIObserver(); + observer.on('willClick', () => { + this.sleepTimer?.refresh(); + }); + } + // [End initSleepTimer] + + async onFoldStatusChange() { + await this.cameraManager.release(); + await this.startCamera(); + this.syncButtonSettings(); + } + + // [Start registerApplicationStateChange] + registerApplicationStateChange() { + this.applicationContext.on('applicationStateChange', { + onApplicationForeground: async () => { + await this.startCamera(); + // [StartExclude registerApplicationStateChange] + this.syncButtonSettings(); + // [EndExclude registerApplicationStateChange] + }, + onApplicationBackground: () => { + // [StartExclude registerApplicationStateChange] + this.previewVM.openPreviewBlur(); + // [EndExclude registerApplicationStateChange] + this.cameraManager.release(); + } + }) + } + + async startCamera() { + const cameraPosition = this.previewVM.getCameraPosition(); + const sceneMode = this.previewVM.getSceneMode(); + await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); + } + // [End registerApplicationStateChange] + + exitApp() { + this.applicationContext.killAllProcesses(); + } + + onPreviewStart() { + this.previewVM.closePreviewBlur(); + } + + initZooms() { + const zoomRange = this.cameraManager.getZoomRange(); + const minZoom = zoomRange[0]; + this.zoomRange = zoomRange; + if (minZoom < this.zooms[0]) { + this.zooms.unshift(minZoom); + } + } + + initRates() { + const frameRates = this.previewManager.getSupportedFrameRates(); + if (frameRates && frameRates[0]) { + const minRate = frameRates[0].min; + const maxRate = frameRates[0].max; + this.previewVM.rates = [minRate, maxRate]; + this.previewVM.currentRate = maxRate; + this.previewManager.setFrameRate(maxRate, maxRate); + }; + } + + syncButtonSettings() { + this.previewManager.setFrameRate(this.previewVM.currentRate, this.previewVM.currentRate); + this.photoManager.enableMovingPhoto(this.isLivePhoto); + this.photoManager.setPhotoOutputCallback(this.isSinglePhoto); + } + + flashBlackAnim() { + this.flashBlackOpacity = 1; + this.isShowBlack = true; + animateToImmediately({ + curve: curves.interpolatingSpring(1, 1, 410, 38), + delay: 50, + onFinish: () => { + this.isShowBlack = false; + this.flashBlackOpacity = 1; + } + }, () => { + this.flashBlackOpacity = 0; + }) + } + + onCaptureClick(): void { + this.flashBlackAnim(); + } + + @Builder + preview() { + // [Start Stack] + Stack({ + alignContent: Alignment.Center + }) { + // [Start XComponent] + // [Start XComponent_gesture] + XComponent({ + type: XComponentType.SURFACE, + controller: this.previewVM.xComponentController + }) + .size({height: '100%', width: '100%'}) + // [StartExclude Stack] + // [StartExclude XComponent_gesture] + .onLoad(async () => { + // [StartExclude XComponent] + await PermissionManager.request(CameraConstant.PERMISSIONS, this.context) + .catch(() => { this.exitApp() }); + // [EndExclude XComponent] + this.previewVM.surfaceId = this.previewVM.xComponentController.getXComponentSurfaceId(); + this.previewVM.setPreviewSize(); + this.previewVM.xComponentController.setXComponentSurfaceRotation({ lock: true }); + // [StartExclude XComponent] + await this.startCamera(); + this.initZooms(); + this.initRates(); + // [EndExclude XComponent] + }) + // [StartExclude XComponent_gesture] + // [End XComponent] + .gesture( + PinchGesture({ fingers: 2 }) + .onActionStart(() => { + this.originZoomBeforePinch = this.currentZoom; + this.isZoomPinching = true; + }) + .onActionUpdate((event: GestureEvent) => { + const targetZoom = this.originZoomBeforePinch * event.scale; + this.currentZoom = limitNumberInRange(targetZoom, this.zoomRange); + this.cameraManager.setZoomRatio(this.currentZoom); + }) + .onActionEnd(() => { + this.isZoomPinching = false; + }) + ) + // [End XComponent_gesture] + .onClick(event => { + this.isFocusBoxVisible = true; + const previewSize = this.previewVM.previewSize; + const cameraPoint = calCameraPoint( + this.getUIContext().vp2px(event.x), + this.getUIContext().vp2px(event.y), + previewSize.width, + previewSize.height + ); + this.cameraManager.setFocusPoint(cameraPoint); + this.cameraManager.setMeteringPoint(cameraPoint); + this.focusBoxPosition = getClampedChildPosition(this.focusBoxSize, { + width: this.getUIContext().px2vp(previewSize.width), + height: this.getUIContext().px2vp(previewSize.height) + }, event); + this.focusBoxTimer.refresh(); + }) + // [EndExclude Stack] + if (this.isGridLineVisible) { + GridLine() + } + // [StartExclude Stack] + if (this.isLevelIndicatorVisible) { + LevelIndicator({ + acc: this.acc + }) + } + // focus box + if (this.isFocusBoxVisible) { + Image($r('app.media.focus_box')) + .width(80) + .height(80) + .position(this.focusBoxPosition) + SymbolGlyph($r('sys.symbol.sun_max')) + .fontSize(this.exposureFontSize) + .fontColor([Color.White]) + .position(this.getExposurePosition()) + } + + if(this.isDelayTakePhoto){ + Text(`${this.photoRemainder}S`) + .fontSize(44) + .fontWeight(FontWeight.Regular) + .fontColor(Color.White) + } + // [EndExclude Stack] + + if (this.isShowBlack) { + Column() + .key('black') + .width('100%') + .height('100%') + .backgroundColor(Color.Black) + .opacity(this.flashBlackOpacity) + } + } + // .width('100%') + // .height('100%') + // [End Stack] + .alignRules({ + middle: { anchor: '__container__', align: HorizontalAlign.Center } + }) + // .width(this.previewVM.getPreviewWidth()) + // .height(this.previewVM.getPreviewHeight()) + // .margin({ top: this.previewVM.getPreviewTop() }) + // .blur(this.previewVM.blurRadius) + // .rotate(this.previewVM.blurRotation) + .size({width: '100%', height: '100%'}) + } + + // [Start wakeupMask] + @Builder + wakeupMask() { + Column() { + Text($r('app.string.wakeup_text')) + .fontColor(Color.White) + .opacity(0.6) + } + // [StartExclude wakeupMask] + .width('100%') + .height('100%') + .backgroundColor(Color.Black) + .justifyContent(FlexAlign.Center) + // [EndExclude wakeupMask] + .onClick(async () => { + this.isSleeping = false; + this.sleepTimer?.refresh(); + await this.startCamera(); + this.syncButtonSettings(); + }) + } + // [End wakeupMask] + + @Builder + gridLineButton() { + SymbolGlyph( + this.isGridLineVisible + ? $r('sys.symbol.camera_assistive_grid') + : $r('sys.symbol.camera_assistive_grid_slash') + ) + .funcButtonStyle() + .onClick(() => { + this.isGridLineVisible = !this.isGridLineVisible; + const message = this.isGridLineVisible ? $r('app.string.grid_line_open') : $r('app.string.grid_line_close'); + showToast(this.getUIContext(), message); + }) + } + + @Builder + levelButton() { + SymbolGlyph($r('sys.symbol.horizontal_level')) + .funcButtonStyle() + .onClick(() => { + this.isLevelIndicatorVisible = !this.isLevelIndicatorVisible; + const message = this.isLevelIndicatorVisible ? $r('app.string.level_open') : $r('app.string.level_close'); + showToast(this.getUIContext(), message); + }) + } + + @Builder + previewImageButton() { + SymbolGlyph(this.isPreviewImageVisible ? $r('sys.symbol.eye') : $r('sys.symbol.eye_slash')) + .funcButtonStyle() + .onClick(() => { + this.isPreviewImageVisible = !this.isPreviewImageVisible; + const message = this.isPreviewImageVisible ? $r('app.string.preview_image_open') : $r('app.string.preview_image_close'); + showToast(this.getUIContext(), message); + }) + } + + getPreviewImageWidth() { + const rotation = display.getDefaultDisplaySync().rotation * 90; + const ratio = this.previewVM.getPreviewRatio(); + const displayRatio = rotation === 90 || rotation === 270 ? 1 / ratio : ratio; + return this.PreviewImageHeight / displayRatio; + } + + getExposurePosition(): Edges { + const focusBoxLeft = this.focusBoxPosition.left as number; + const focusBoxTop = this.focusBoxPosition.top as number; + const exposureWidth = this.exposureFontSize; + const exposureHeight = this.exposureFontSize; + const focusBoxWidth = this.focusBoxSize.width; + const focusBoxHeight = this.focusBoxSize.height; + const previewWidth = this.getUIContext().px2vp(this.previewVM.previewSize.width); + const GAP = 10; + const top = focusBoxTop - exposureHeight / 2 + focusBoxHeight / 2; + const left = focusBoxLeft > previewWidth / 2 + ? focusBoxLeft - GAP - exposureWidth + : focusBoxLeft + focusBoxWidth + GAP; + return { top, left }; + } + + @Builder + previewImageView() { + Image(this.previewImage) + .width(this.getPreviewImageWidth()) + .height(this.PreviewImageHeight) + .alignRules({ + bottom: { anchor: '__container__', align: VerticalAlign.Bottom }, + left: { anchor: '__container__', align: HorizontalAlign.Start } + }) + .margin({ + bottom: 10, + left: 10 + }) + } + + @Builder + funcButtonsView() { + Column({ space: 24 }) { + this.gridLineButton() + this.levelButton() + this.previewImageButton() + } + .alignRules({ + top: { anchor: 'settingButtonsView', align: VerticalAlign.Bottom }, + right: { anchor: 'settingButtonsView', align: HorizontalAlign.End } + }) + .margin({ + top: 40, + right: 10 + }) + } + + build() { + RelativeContainer() { + this.preview() + if (this.isPreviewImageVisible) { + this.previewImageView() + } + this.funcButtonsView() + SettingButtonsView({ + previewVM: this.previewVM, + cameraManager: this.cameraManager, + previewManager: this.previewManager, + photoManager: this.photoManager, + videoManager: this.videoManager, + photoDelayTime: this.photoDelayTime, + isSinglePhoto: this.isSinglePhoto, + isLivePhoto: this.isLivePhoto + }) + if (!this.photoRemainder) { + if (!this.previewVM.isFront) { + ZoomButtonsView({ + cameraManager: this.cameraManager, + zoomRange: this.zoomRange, + zooms: this.zooms, + currentZoom: this.currentZoom + }) + } + ModeButtonsView({ + previewVM: this.previewVM, + photoManager: this.photoManager, + videoManager: this.videoManager, + cameraManager: this.cameraManager, + syncButtonSettings: () => { + this.syncButtonSettings(); + } + }) + OperateButtonsView({ + previewVM: this.previewVM, + cameraManager: this.cameraManager, + photoManager: this.photoManager, + videoManager: this.videoManager, + isDelayTakePhoto: this.isDelayTakePhoto, + photoDelayTime: this.photoDelayTime, + photoRemainder: this.photoRemainder, + syncButtonSettings: () => { + this.syncButtonSettings(); + } + }) + } + if (this.isSleeping) { + this.wakeupMask() + } + } + .height('100%') + .width('100%') + .backgroundColor(Color.Black) + .onClick(() => {}) + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/CommonUtil.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/CommonUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..fcf47ba52d1008455efad159b4c1ef32803399b5 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/CommonUtil.ets @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025 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 { Point } from '@kit.TestKit'; +import { camera } from '@kit.CameraKit'; +import { display } from '@kit.ArkUI'; + +export function limitNumberInRange(src: number, range: number[]) { + if (range.length < 2) return src; + if (src < range[0]) return range[0]; + if (src > range[1]) return range[1]; + return src; +} + +// find start index the target in which range +// eg: target: 1.5 arr: [0, 1, 5, 10] return 1 +export function findRangeIndex(target: number, arr: number[]) { + if (arr.length === 0) { + return -1; + } + if (target >= arr[arr.length - 1]) { + return arr.length - 1; + } + return [...arr].sort((a, b) => a - b).findIndex((n, i) => { + return target >= n && target < arr[i + 1]; + }); +} + +// Math floor float by digit +// eg: toFixed(9.97, 1) -> 9.9 +export function toFixed(num: number, digit: number): string { + const scale = 10 ** digit; + return (Math.floor(num * scale) / scale).toFixed(digit); +} + +// [Start getClampedChildPosition] +// cal absolute position in parent area +export function getClampedChildPosition(childSize: Size, parentSize: Size, point: Point): Edges { + // center point + let left = point.x - childSize.width / 2; + let top = point.y - childSize.height / 2; + // limit in left + if (left < 0) { + left = 0; + } + // limit in right + if (left + childSize.width > parentSize.width) { + left = parentSize.width - childSize.width; + } + // limit in top + if (top < 0) { + top = 0; + } + // limit in bottom + if (top + childSize.height > parentSize.height) { + top = parentSize.height - childSize.height; + } + return { left, top }; +} +// [End getClampedChildPosition] + +export function showToast( + UIContext: UIContext, + message: ResourceStr = '', + duration = 2000, + alignment = Alignment.Top, + offset: Offset = { dx: 0, dy: 300 } +) { + UIContext.getPromptAction().openToast({ + message, + duration, + alignment, + offset + }); +} + +// [Start calCameraPoint] +export function calCameraPoint(eventX: number, eventY: number, width: number, height: number): camera.Point { + const displayRotation = display.getDefaultDisplaySync().rotation * 90; + if (displayRotation === 0) { + return { x: eventY / height, y: 1 - eventX / width }; + } + if (displayRotation === 90) { + return { x: 1 - eventX / width, y: 1 - eventY / height }; + } + if (displayRotation === 180) { + return { x: 1 - eventY / height, y: eventX / width }; + } + return { x: eventX / width, y: eventY / height }; +} +// [End calCameraPoint] \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/PermissionManager.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/PermissionManager.ets new file mode 100644 index 0000000000000000000000000000000000000000..c65190ea0ecc9ad527f4e576bd6db88a79d4ef40 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/PermissionManager.ets @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2025 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 { abilityAccessCtrl, Context, Permissions } from '@kit.AbilityKit'; +import { Logger } from 'commons'; + +const TAG = 'PermissionManager'; + +// [Start request_permissions] +class PermissionManager { + private static atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); + + static async request(permissions: Permissions[], context: Context) { + const data = await PermissionManager.atManager.requestPermissionsFromUser(context, permissions); + const grantStatus: number[] = data.authResults; + const deniedPermissions = permissions.filter((_, i) => grantStatus[i] !== 0); + for (const permission of deniedPermissions) { + const secondGrantStatus = await PermissionManager.atManager.requestPermissionOnSetting(context, [permission]); + if (secondGrantStatus[0] !== 0) { + Logger.error(TAG, 'permission denied'); + throw new Error('permission denied'); + } + } + } +} +// [End request_permissions] + +export default PermissionManager; \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/RefreshableTimer.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/RefreshableTimer.ets new file mode 100644 index 0000000000000000000000000000000000000000..4956219f7c1c0ff6d3a80d1c46978f556ca02768 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/RefreshableTimer.ets @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 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. + */ + +// [Start RefreshableTimer] +class RefreshableTimer { + private timerId?: number; + private readonly timeout: number; + private callback: () => void; + private isActive: boolean = false; + + constructor(callback: () => void, timeout: number) { + this.callback = callback; + this.timeout = timeout; + } + + start(): void { + this.timerId = setTimeout(() => { + this.callback(); + this.isActive = false; + }, this.timeout); + this.isActive = true; + } + + clear(): void { + clearTimeout(this.timerId); + this.timerId = undefined; + this.isActive = false; + } + + refresh(): void { + this.clear(); + this.start(); + } + + isRunning(): boolean { + return this.isActive; + } +} +// [End RefreshableTimer] + +export default RefreshableTimer; diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/WindowUtil.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/WindowUtil.ets new file mode 100644 index 0000000000000000000000000000000000000000..9d54a733dca804e4bf57fbf7831e3b4c50f6f284 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/utils/WindowUtil.ets @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2025 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 { display, window } from '@kit.ArkUI'; +import { Logger } from 'commons'; + +const TAG = 'WindowUtil' + +class WindowUtil { + static async enterImmersive(window: window.Window): Promise { + try { + await window.setWindowLayoutFullScreen(true); + await window.setWindowSystemBarEnable([]); + } catch (exception) { + Logger.error(TAG, 'Failed to enter immersive. Cause:', JSON.stringify(exception)); + } + } + + // Given a ratio, obtain the maximum display width and height based on the screen width and height. + static getMaxDisplaySize(ratio: number): Size { + const defaultDisplay: display.Display = display.getDefaultDisplaySync(); + const windowWidth: number = defaultDisplay.width; + const windowHeight: number = defaultDisplay.height; + const rotation = display.getDefaultDisplaySync().rotation * 90; + if (rotation === 90 || rotation === 270) { + ratio = 1 / ratio; + } + // Calculate the height based on the screen width. + const calculatedHeight = windowWidth * ratio; + if (calculatedHeight <= windowHeight) { + return { + width: windowWidth, + height: calculatedHeight + }; + } else { + return { + width: windowHeight / ratio, + height: windowHeight + }; + } + } + + static getWindowRatio(): number { + const defaultDisplay: display.Display = display.getDefaultDisplaySync(); + return defaultDisplay.height / defaultDisplay.width; + } +} + +export default WindowUtil; \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/viewmodels/PreviewViewModel.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/viewmodels/PreviewViewModel.ets new file mode 100644 index 0000000000000000000000000000000000000000..c277ea257d1d60e83b7a1a068b42d64f2af9ad39 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/viewmodels/PreviewViewModel.ets @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2025 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 { curves, display } from '@kit.ArkUI'; +import { camera } from '@kit.CameraKit'; +import WindowUtil from '../utils/WindowUtil'; +import CameraConstant from '../constants/Constants'; + +export enum CameraMode { + PHOTO, + VIDEO +} + +/** + * States and methods related to preview. + */ +class PreviewViewModel { + private uiContext: UIContext; + // [Start isFront] + isFront: boolean = false; + // [StartExclude isFront] + cameraMode: CameraMode = CameraMode.PHOTO; + xComponentController: XComponentController = new XComponentController(); + surfaceId: string = ''; + previewSize: Size = WindowUtil.getMaxDisplaySize(CameraConstant.RATIO_PHOTO); + rates?: number[] = []; + currentRate: number = 0; + blurRadius: number = 0; + blurRotation: RotateOptions = { y: 0.5, angle: 0}; + + constructor(uiContext: UIContext) { + this.uiContext = uiContext; + } + + // [EndExclude isFront] + getCameraPosition() { + return this.isFront + ? camera.CameraPosition.CAMERA_POSITION_FRONT + : camera.CameraPosition.CAMERA_POSITION_BACK; + } + // [End isFront] + + getPreviewRatio() { + return this.cameraMode === CameraMode.PHOTO + ? CameraConstant.RATIO_PHOTO + : CameraConstant.RATIO_VIDEO; + } + + getSceneMode() { + return this.cameraMode === CameraMode.PHOTO + ? camera.SceneMode.NORMAL_PHOTO + : camera.SceneMode.NORMAL_VIDEO; + } + + // [Start getProfile] + getProfile: (cameraOrientation: number, picFormat: camera.CameraFormat) => camera.Profile = (cameraOrientation, picFormat) => { + const displaySize: Size = WindowUtil.getMaxDisplaySize(this.getPreviewRatio()); + const displayRotation = display.getDefaultDisplaySync().rotation * 90; + const isRevert = (cameraOrientation + displayRotation) % 180 !== 0; + return { + // format: camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP, + format: picFormat, + size: { + height: isRevert ? displaySize.width : displaySize.height, + width: isRevert ? displaySize.height : displaySize.width + } + }; + } + // [End getProfile] + + // [Start setPreviewSize] + setPreviewSize() { + const displaySize: Size = WindowUtil.getMaxDisplaySize(this.getPreviewRatio()); + this.previewSize = displaySize; + this.xComponentController.setXComponentSurfaceRect({ + surfaceWidth: displaySize.width, + surfaceHeight: displaySize.height + }); + } + // [End setPreviewSize] + + getPreviewTop() { + const previewRatio = this.getPreviewRatio(); + return WindowUtil.getWindowRatio() > previewRatio ? 85 : 0; + } + + getPreviewWidth() { + return this.uiContext.px2vp(this.previewSize.width); + } + + getPreviewHeight() { + return this.uiContext.px2vp(this.previewSize.height); + } + + isPhotoMode() { + return this.cameraMode === CameraMode.PHOTO; + } + + isVideoMode() { + return this.cameraMode === CameraMode.VIDEO; + } + + isCurrentCameraMode(mode: CameraMode) { + return this.cameraMode === mode; + } + + openPreviewBlur() { + animateToImmediately({ + duration: 200, + curve: Curve.Friction + }, () => { + this.blurRadius = 150; + }); + } + + rotatePreviewBlur() { + animateToImmediately({ + delay: 50, + duration: 200, + curve: curves.cubicBezierCurve(0.2, 0, 0.83, 1), + onFinish: () => { + this.rotatePreviewBlurSecond(); + } + }, () => { + this.blurRotation = { y: 0.5, angle: 90 }; + }); + } + + rotatePreviewBlurSecond() { + this.blurRotation = { y: 0.5, angle: 270 }; + animateToImmediately({ + duration: 200, + curve: curves.cubicBezierCurve(0.17, 0, 0.2, 1), + onFinish: () => { + this.blurRotation = { y: 0.5, angle: 0 }; + } + }, () => { + this.blurRotation = { y: 0.5, angle: 360 }; + }); + } + + closePreviewBlur() { + animateToImmediately({ + duration: 200, + curve: Curve.FastOutSlowIn + }, () => { + this.blurRadius = 0; + }); + } +} + +export default PreviewViewModel; \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/ModeButtonsView.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/ModeButtonsView.ets new file mode 100644 index 0000000000000000000000000000000000000000..0a8bd9352ccf756425e974a4cc71cfefcf4a114e --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/ModeButtonsView.ets @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025 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 { CameraManager, PhotoManager, VideoManager } from 'camera'; +import PreviewViewModel, { CameraMode } from '../viewmodels/PreviewViewModel'; + +export interface CameraModeButton { + title: ResourceStr; + mode: CameraMode; + onClick?: () => void; +} + +@Component +struct ModeButtonsView { + private cameraModeButtons: CameraModeButton[] = [ + { + title: $r('app.string.photo'), + mode: CameraMode.PHOTO + }, + { + title: $r('app.string.video'), + mode: CameraMode.VIDEO + } + ] + @Link previewVM: PreviewViewModel; // Do not use @prop, otherwise deep copying, some underlying data will be lost. + @Require cameraManager: CameraManager; + @Require photoManager: PhotoManager; + @Require videoManager: VideoManager; + @Require syncButtonSettings: () => void; + + build() { + Row() { + ForEach(this.cameraModeButtons, (modeBtn: CameraModeButton) => { + Text(modeBtn.title) + .fontColor(Color.White) + .fontSize(14) + .fontWeight(this.previewVM.isCurrentCameraMode(modeBtn.mode) ? FontWeight.Bold : FontWeight.Normal) + .onClick(async () => { + if (modeBtn.onClick) { + modeBtn.onClick(); + } else { + if (this.previewVM.isCurrentCameraMode(modeBtn.mode)) { + return; + } + this.previewVM.openPreviewBlur(); + this.previewVM.cameraMode = modeBtn.mode; + this.previewVM.setPreviewSize(); + const sceneMode = this.previewVM.getSceneMode(); + const cameraPosition = this.previewVM.getCameraPosition(); + await this.cameraManager.release(); + this.photoManager.setIsActive(this.previewVM.isPhotoMode() ? true : false); + this.videoManager.setIsActive(this.previewVM.isPhotoMode() ? false : true); + await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); + this.syncButtonSettings(); + } + }) + }, (modeBtn: CameraModeButton) => modeBtn.mode.toString()) + } + .id('modeButtonsView') + .width('40%') + .justifyContent(FlexAlign.SpaceAround) + .alignRules({ + bottom: { anchor: 'operateButtonsView', align: VerticalAlign.Top }, + middle: { anchor: 'operateButtonsView', align: HorizontalAlign.Center } + }) + .margin({ bottom: 40 }) + } +} + +export default ModeButtonsView; \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/OperateButtonsView.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/OperateButtonsView.ets new file mode 100644 index 0000000000000000000000000000000000000000..2bd8589fa633efe0c78194f56de8d9b8e7261e02 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/OperateButtonsView.ets @@ -0,0 +1,270 @@ +/* + * Copyright (c) 2025 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 { image } from '@kit.ImageKit'; +import { common } from '@kit.AbilityKit'; +import { AVRecorderState, CameraManager, PhotoManager, VideoManager } from 'camera'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; + +@Component +struct OperateButtonsView { + @Link isDelayTakePhoto: boolean; + @Link previewVM: PreviewViewModel; + @Require cameraManager: CameraManager; + @Link videoManager: VideoManager; // Do not use @prop, otherwise deep copying, some underlying data will be lost. + @Require photoManager: PhotoManager; + @Prop @Require photoDelayTime: number; + @Link photoRemainder: number; + private photoDelayTimer:number = 0; + @State thumbnail: image.PixelMap | string = ''; + @State thumbnailUrl: string = ''; + @Require syncButtonSettings: () => void; + private context = this.getUIContext().getHostContext() as common.UIAbilityContext; + private setThumbnail: (pixelMap: image.PixelMap, url: string) => void = (pixelMap: image.PixelMap, url: string) => { + this.thumbnail = pixelMap + this.thumbnailUrl = url + } + @StorageLink('captureClick') captureClickFlag: number = 0; + + aboutToAppear(): void { + this.photoManager.setCallback(this.setThumbnail); + this.videoManager.setVideoCallback(this.setThumbnail); + } + + @Builder + photoButton() { + Column() { + Column() + .width(48) + .height(48) + .borderRadius('50%') + .border({ + color: Color.White, + width: 24 + }) + } + .id('Capture') + .width(64) + .height(64) + .borderRadius('50%') + .border({ + color: Color.White, + width: 1 + }) + .justifyContent(FlexAlign.Center) + .onClick(() => { + if (this.photoDelayTime) { + this.isDelayTakePhoto = true; + this.photoRemainder = this.photoDelayTime; + this.photoDelayTimer = setInterval(()=>{ + this.photoRemainder--; + if(this.photoRemainder === 0){ + this.photoManager.capture(this.previewVM.isFront); + this.captureClickFlag++; + this.isDelayTakePhoto = false; + clearTimeout(this.photoDelayTimer); + } + },1000) + } else { + this.photoManager.capture(this.previewVM.isFront); + this.captureClickFlag++; + } + }) + } + + @Builder + videoStartButton() { + Column() { + Column() + .width(52) + .height(52) + .backgroundColor(Color.Red) + .borderRadius('50%') + .border({ + color: Color.White, + width: 8 + }) + } + .id('VideoStartButton') + .width(64) + .height(64) + .borderRadius('50%') + .border({ + color: Color.White, + width: 1 + }) + .justifyContent(FlexAlign.Center) + .onClick(() => { + this.videoManager.start(this.previewVM.isFront); + }) + } + + @Builder + videoStopButton() { + Column() { + Column() + .width(28) + .height(28) + .borderRadius(10) + .backgroundColor(Color.Red) + } + .id('VideoStopButton') + .width(64) + .height(64) + .borderRadius('50%') + .border({ + color: Color.White, + width: 1 + }) + .justifyContent(FlexAlign.Center) + .onClick(async () => { + if (this.videoManager.state === AVRecorderState.STARTED || this.videoManager.state === AVRecorderState.PAUSED) { + await this.videoManager.stop(); + await this.cameraManager.release(); + const cameraPosition = this.previewVM.getCameraPosition(); + const sceneMode = this.previewVM.getSceneMode(); + await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); + this.syncButtonSettings(); + } + }) + } + + @Builder + videoPauseButton() { + SymbolGlyph($r('sys.symbol.pause')) + .fontColor([Color.White]) + .fontSize(22) + .fontWeight(FontWeight.Bolder) + .padding(11) + .borderWidth(1) + .borderColor(Color.White) + .borderRadius('50%') + .symbolEffect(new ReplaceSymbolEffect(EffectScope.WHOLE), true) + .onClick(async () => { + this.videoManager.pause(); + }) + } + + @Builder + videoResumeButton() { + SymbolGlyph($r('sys.symbol.play')) + .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR) + .fontColor([Color.White]) + .fontSize(22) + .padding(11) + .borderWidth(1) + .borderColor(Color.White) + .borderRadius('50%') + .onClick(async () => { + this.videoManager.resume(); + }) + } + + @Builder + thumbnailButton() { + Image(this.thumbnail) + .id('Thumbnail') + .width(48) + .height(48) + .borderRadius('50%') + .borderWidth(this.thumbnail ? 1 : 0) + .borderColor(Color.White) + .clip(true) + .onClick(()=>{ + if(this.thumbnailUrl){ + this.context.startAbility({ + parameters: { uri: this.thumbnailUrl }, + action: 'ohos.want.action.viewData', + bundleName: 'com.huawei.hmos.photos', + abilityName: 'com.huawei.hmos.photos.MainAbility' + }); + } + }) + } + + @Builder + delayTakePhotoCancelButton() { + SymbolGlyph($r('sys.symbol.pause')) + .fontColor([Color.White]) + .fontSize(22) + .fontWeight(FontWeight.Bolder) + .padding(11) + .borderWidth(1) + .borderColor(Color.White) + .borderRadius('50%') + .symbolEffect(new ReplaceSymbolEffect(EffectScope.WHOLE), true) + .onClick(async () => { + this.isDelayTakePhoto = false; + clearTimeout(this.photoDelayTimer); + this.photoRemainder = 0; + }) + } + + // [Start toggleCameraPositionButton] + @Builder + toggleCameraPositionButton() { + Image($r('app.media.toggle_position')) + .width(48) + .height(48) + .onClick(async () => { + // [StartExclude toggleCameraPositionButton] + this.previewVM.openPreviewBlur(); + this.previewVM.rotatePreviewBlur(); + // [EndExclude toggleCameraPositionButton] + this.previewVM.isFront = !this.previewVM.isFront; + const cameraPosition = this.previewVM.getCameraPosition(); + const sceneMode = this.previewVM.getSceneMode(); + await this.cameraManager.release(); + await this.cameraManager.start(this.previewVM.surfaceId, cameraPosition, sceneMode, this.previewVM.getProfile); + // [StartExclude toggleCameraPositionButton] + this.syncButtonSettings(); + // [EndExclude toggleCameraPositionButton] + }) + } + // [End toggleCameraPositionButton] + + build() { + Row() { + this.thumbnailButton() + if (this.previewVM.isPhotoMode()) { + this.photoButton() + } else { + if (this.videoManager.isRecording()) { + this.videoStopButton() + } else { + this.videoStartButton() + } + } + if (!this.videoManager.isRecording()) { + this.toggleCameraPositionButton() + } + if (this.previewVM.isVideoMode() && this.videoManager.state === AVRecorderState.STARTED) { + this.videoPauseButton() + } else if (this.previewVM.isVideoMode() && this.videoManager.state === AVRecorderState.PAUSED) { + this.videoResumeButton() + } + } + .justifyContent(FlexAlign.SpaceAround) + .id('operateButtonsView') + .alignRules({ + bottom: { anchor: '__container__', align: VerticalAlign.Bottom }, + middle: { anchor: '__container__', align: HorizontalAlign.Center }, + }) + .margin({ bottom: 90 }) + .width('90%') + } +} + +export default OperateButtonsView; \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/SettingButtonsView.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/SettingButtonsView.ets new file mode 100644 index 0000000000000000000000000000000000000000..e388a0dea1b29fb40ee8be1c222dcd95d1821ff5 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/SettingButtonsView.ets @@ -0,0 +1,250 @@ +/* + * Copyright (c) 2025 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 '@kit.CameraKit'; +import { AVRecorderState, CameraManager, PhotoManager, PreviewManager, VideoManager } from 'camera'; +import { showToast } from '../utils/CommonUtil'; +import PreviewViewModel from '../viewmodels/PreviewViewModel'; + +interface FlashItem { + mode: camera.FlashMode; + image: Resource; + title: ResourceStr; + toast: ResourceStr; +} + +@Component +struct SettingButtonsView { + private flashItems: FlashItem[] = [ + { + mode: camera.FlashMode.FLASH_MODE_CLOSE, + image: $r('sys.symbol.camera_flash_slash'), + title: 'off', + toast: $r('app.string.flash_close') + }, + { + mode: camera.FlashMode.FLASH_MODE_OPEN, + image: $r('sys.symbol.camera_flash'), + title: 'on', + toast: $r('app.string.flash_open') + }, + { + mode: camera.FlashMode.FLASH_MODE_AUTO, + image: $r('sys.symbol.camera_flash_auto'), + title: 'auto', + toast: $r('app.string.flash_auto') + }, + { + mode: camera.FlashMode.FLASH_MODE_ALWAYS_OPEN, + image: $r('sys.symbol.lightbulb_1'), + title: 'always_on', + toast: $r('app.string.flash_always') + } + ]; + private photoFlashModes: camera.FlashMode[] = [ + camera.FlashMode.FLASH_MODE_CLOSE, + camera.FlashMode.FLASH_MODE_OPEN, + camera.FlashMode.FLASH_MODE_AUTO, + camera.FlashMode.FLASH_MODE_ALWAYS_OPEN + ]; + private videoFlashModes: camera.FlashMode[] = [ + camera.FlashMode.FLASH_MODE_CLOSE, + camera.FlashMode.FLASH_MODE_ALWAYS_OPEN + ]; + @State flashMode: camera.FlashMode = camera.FlashMode.FLASH_MODE_CLOSE; + @Link isLivePhoto: boolean; + @Require cameraManager: CameraManager; + @Require previewManager: PreviewManager; + @Require photoManager: PhotoManager; + @Link videoManager: VideoManager; + @Link photoDelayTime: number; + @State isStabilizationEnabled: boolean = false; + @Link isSinglePhoto: boolean; + @Link previewVM: PreviewViewModel; + + getFlashItem(mode: camera.FlashMode) { + return this.flashItems.find(item => item.mode === mode); + } + + @Builder + flashButton(flashModes: camera.FlashMode[]) { + SymbolGlyph(this.getFlashItem(this.flashMode)?.image) + .fontSize(22) + .fontColor([Color.White]) + .bindMenu(flashModes.map(mode => { + const flashItem = this.getFlashItem(mode)!; + const menuElement: MenuElement = { + value: flashItem.title, + action: () => { + this.flashMode = mode!; + this.cameraManager.setFlashMode(mode); + showToast(this.getUIContext(), flashItem.toast); + } + }; + return menuElement; + })) + } + + @Builder + videoTimerBuilder() { + if (this.videoManager.isRecording()) { + Row({ space: 5 }) { + SymbolGlyph(this.videoManager.state === AVRecorderState.STARTED ? $r('sys.symbol.record_circle') : + $r('sys.symbol.pause')) + .fontSize(22) + .fontColor(this.videoManager.state === AVRecorderState.STARTED ? [Color.Red, 'rgba(255,0,0,0)'] : + [Color.White]) + .renderingStrategy(SymbolRenderingStrategy.MULTIPLE_COLOR) + Text(this.videoManager.state === AVRecorderState.STARTED ? $r('app.string.recording') : $r('app.string.paused')) + .fontColor(Color.White) + .fontSize(12) + } + } + } + + @Builder + livePhotoButton() { + SymbolGlyph(this.isLivePhoto + ? $r('sys.symbol.livephoto') + : $r('sys.symbol.livephoto_slash')) + .onClick(() => { + this.isLivePhoto = !this.isLivePhoto; + this.photoManager.enableMovingPhoto(this.isLivePhoto); + const message = this.isLivePhoto ? $r('app.string.moving_open') : $r('app.string.moving_close'); + showToast(this.getUIContext(), message); + }) + .fontSize(22) + .fontColor([Color.White]) + } + + @Builder + rateButton() { + if (this.previewVM.rates) { + Text(`${this.previewVM.currentRate} fps`) + .fontColor(Color.White) + .fontSize(16) + .bindMenu(this.previewVM.rates.map(rate => { + const menuElement: MenuElement = { + value: rate + 'fps', + action: () => { + this.previewManager.setFrameRate(rate, rate); + this.previewVM.currentRate = rate; + showToast(this.getUIContext(), $r('app.string.preview_rate', rate + 'fps')); + } + }; + return menuElement; + })) + } + } + + @Builder + delayPhotoButton(photoDelayTimeElements: MenuElement[]) { + if (this.photoDelayTime) { + Text(`${this.photoDelayTime}s`) + .fontColor(Color.White) + .fontSize(16) + .bindMenu(photoDelayTimeElements) + } else { + SymbolGlyph($r('sys.symbol.time_lapse')) + .fontSize(22) + .fontColor([Color.White]) + .bindMenu(photoDelayTimeElements) + } + } + + getPhotoDelayTimeElements(): MenuElement[] { + const menuTextMap: Map = new Map([ + ['off', 0], + ['2s', 2], + ['5s', 5], + ['10s', 10] + ]) + return Array.from(menuTextMap.keys()).map(text => { + const time = menuTextMap.get(text); + const menuElement: MenuElement = { + value: text, + action: () => { + this.photoDelayTime = time!; + const message = time ? $r('app.string.delay', text) : $r('app.string.delay_close'); + showToast(this.getUIContext(), message); + } + }; + return menuElement; + }); + } + + @Builder + stabilizationButton() { + SymbolGlyph(this.isStabilizationEnabled + ? $r('sys.symbol.motion_stabilization') + : $r('sys.symbol.motion_stabilization_slash')) + .onClick(() => { + this.isStabilizationEnabled = !this.isStabilizationEnabled; + this.cameraManager.setVideoStabilizationMode(camera.VideoStabilizationMode.AUTO); + const message = this.isStabilizationEnabled ? $r('app.string.stabilization_enable') : $r('app.string.stabilization_disabled'); + // showToast(this.getUIContext(), message); + }) + .fontSize(22) + .fontColor([Color.White]) + } + + @Builder + togglePhotoModeButton() { + SymbolGlyph(this.isSinglePhoto + ? $r('sys.symbol.picture') + : $r('sys.symbol.picture_on_square_1')) + .onClick(() => { + this.isSinglePhoto = !this.isSinglePhoto; + this.photoManager.setPhotoOutputCallback(this.isSinglePhoto); + if (this.isSinglePhoto) { + this.isLivePhoto = false; + } + this.photoManager.enableMovingPhoto(this.isLivePhoto); + const message = this.isSinglePhoto ? $r('app.string.photo_single') : $r('app.string.photo_double'); + showToast(this.getUIContext(), message); + }) + .fontSize(22) + .fontColor([Color.White]) + } + + build() { + Row() { + if (this.previewVM.isPhotoMode()) { + this.rateButton() + this.flashButton(this.photoFlashModes) + this.delayPhotoButton(this.getPhotoDelayTimeElements()) + if (!this.isSinglePhoto) { + this.livePhotoButton() + } + this.togglePhotoModeButton() + } else { + if (this.videoManager.isRecording()) { + this.videoTimerBuilder() + } else { + this.rateButton() + this.flashButton(this.videoFlashModes) + this.stabilizationButton() + } + } + } + .id('settingButtonsView') + .width('100%') + .margin({ top: 50 }) + .justifyContent(FlexAlign.SpaceAround) + .alignRules({ top: { anchor: '__container__', align: VerticalAlign.Top } }) + } +} + +export default SettingButtonsView; \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/ZoomButtonsView.ets b/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/ZoomButtonsView.ets new file mode 100644 index 0000000000000000000000000000000000000000..ccdef4750b5fa7025cb7e6a40a42a97b6cb54012 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/ets/views/ZoomButtonsView.ets @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2025 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 { CameraManager } from 'camera'; +import { findRangeIndex, toFixed } from '../utils/CommonUtil'; + +@Component +struct ZoomButtonsView { + @Prop @Require zooms: number[]; + @Prop @Require zoomRange: number[] = []; + @Link currentZoom: number; + @Require cameraManager: CameraManager; + + getZoomButtonText(zoom: number, index: number): string { + const minZoom = this.zoomRange[0]; + const currentZoomIndex: number = findRangeIndex(this.currentZoom, this.zooms); + if (index === 0 && (this.currentZoom === minZoom || currentZoomIndex !== index)) { + return 'w'; + } + if (this.currentZoom === zoom || currentZoomIndex !== index) { + return `${zoom}x`; + } + return `${toFixed(this.currentZoom, 1)}x`; + } + + getZoomButtonBorderWidth(index: number): number { + const currentZoomIndex: number = findRangeIndex(this.currentZoom, this.zooms); + return currentZoomIndex === index ? 1.5 : 0; + } + + build() { + Row({ space: 15 }) { + ForEach(this.zooms, (zoom: number, index: number) => { + Text(this.getZoomButtonText(zoom, index)) + .width(36) + .height(36) + .fontColor(Color.White) + .fontSize(10) + .fontWeight(FontWeight.Bold) + .borderWidth(this.getZoomButtonBorderWidth(index)) + .borderColor(Color.White) + .borderRadius('50%') + .textAlign(TextAlign.Center) + .onClick(() => { + this.cameraManager.setSmoothZoom(zoom) + this.currentZoom = zoom + }) + }, (zoom: number) => zoom.toString()) + } + .margin({ bottom: 40 }) + .alignRules({ + bottom: { anchor: 'modeButtonsView', align: VerticalAlign.Top }, + middle: { anchor: 'modeButtonsView', align: HorizontalAlign.Center } + }) + } +} +export default ZoomButtonsView; \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/module.json5 b/CameraKit/RotationDisplayCamera/entry/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..95172c683d8d5e3060a561c4bbb1c1d389725f59 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/module.json5 @@ -0,0 +1,111 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": [ + "default", + "tablet" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ], + "orientation": "follow_desktop" + } + ], + // [Start permissions] + // [Start permissions_acc] + "requestPermissions": [ + // [StartExclude permissions_acc] + { + "name": "ohos.permission.CAMERA", + "reason": "$string:permission_CAMERA", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + // [StartExclude permissions] + { + "name": "ohos.permission.MICROPHONE", + "reason": "$string:reason_microphone", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + { + "name": "ohos.permission.MEDIA_LOCATION", + "reason": "$string:reason_media_location", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + { + "name": "ohos.permission.WRITE_IMAGEVIDEO", + "reason": "$string:reason_write_imagevideo", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + { + "name": "ohos.permission.READ_IMAGEVIDEO", + "reason": "$string:reason_read_imagevideo", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + { + "name": "ohos.permission.APPROXIMATELY_LOCATION", + "reason": "$string:permission_LOCATION", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + }, + // [EndExclude permissions_acc] + { + "name": "ohos.permission.ACCELEROMETER", + "reason": "$string:permission_SENSOR", + "usedScene": { + "abilities": [ + "EntryAbility" + ] + } + } + // [EndExclude permissions] + ] + // [End permissions] + // [End permissions_acc] + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/element/color.json b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..3c712962da3c2751c2b9ddb53559afcbd2b54a02 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#FFFFFF" + } + ] +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/element/float.json b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/element/float.json new file mode 100644 index 0000000000000000000000000000000000000000..33ea22304f9b1485b5f22d811023701b5d4e35b6 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/element/float.json @@ -0,0 +1,8 @@ +{ + "float": [ + { + "name": "page_text_font_size", + "value": "50fp" + } + ] +} diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/element/string.json b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..32d0de3f681b25d4009cffd841f63483b0e3a0d8 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/element/string.json @@ -0,0 +1,140 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "CustomCamera" + }, + { + "name": "permission_CAMERA", + "value": "For camera operations" + }, + { + "name": "reason_microphone", + "value": "For camera video recording" + }, + { + "name": "reason_media_location", + "value": "For scenarios where the camera retrieves media information" + }, + { + "name": "reason_write_imagevideo", + "value": "For reading and writing media files with the camera" + }, + { + "name": "reason_read_imagevideo", + "value": "For camera media file read/write" + }, + { + "name": "permission_SENSOR", + "value": "For sensors" + }, + { + "name": "recording", + "value": "Recording in progress" + }, + { + "name": "paused", + "value": "Paused" + }, + { + "name": "wakeup_text", + "value": "Tap the screen to wake up the camera" + }, + { + "name": "photo", + "value": "Photo" + }, + { + "name": "video", + "value": "Video" + }, + { + "name": "preview_rate", + "value": "Preview frame rate%d" + }, + { + "name": "delay", + "value": "Time-lapse%d" + }, + { + "name": "delay_close", + "value": "Off time-lapse" + }, + { + "name": "moving_open", + "value": "Live photo mode is on" + }, + { + "name": "moving_close", + "value": "Live photo mode is off" + }, + { + "name": "photo_single", + "value": "Single-segment shooting" + }, + { + "name": "photo_double", + "value": "Dual-segment shooting" + }, + { + "name": "stabilization_enable", + "value": "Video stabilization is on" + }, + { + "name": "stabilization_disabled", + "value": "Video stabilization is off" + }, + { + "name": "grid_line_open", + "value": "Grid lines are enabled" + }, + { + "name": "grid_line_close", + "value": "Grid lines are disabled" + }, + { + "name": "level_open", + "value": "The spirit level is enabled" + }, + { + "name": "level_close", + "value": "The spirit level is disabled" + }, + { + "name": "flash_auto", + "value": "Flash is in auto mode" + }, + { + "name": "flash_close", + "value": "The flash is off" + }, + { + "name": "flash_always", + "value": "The flash is always on" + }, + { + "name": "flash_open", + "value": "The flash is on" + }, + { + "name": "preview_image_open", + "value": "Dual-channel preview has been enabled" + }, + { + "name": "preview_image_close", + "value": "Dual-channel preview has been disabled" + }, + { + "name": "permission_LOCATION", + "value": "For location" + } + ] +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/background.png b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..923f2b3f27e915d6871871deea0420eb45ce102f Binary files /dev/null and b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/background.png differ diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/flash_10s.png b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/flash_10s.png new file mode 100644 index 0000000000000000000000000000000000000000..b6de041fe17fa2dc0ff7a6784e0bee0526e2277f Binary files /dev/null and b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/flash_10s.png differ diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/flash_2s.png b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/flash_2s.png new file mode 100644 index 0000000000000000000000000000000000000000..183c0ec5c8b99261dbf6130373cb60b08534f672 Binary files /dev/null and b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/flash_2s.png differ diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/flash_5s.png b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/flash_5s.png new file mode 100644 index 0000000000000000000000000000000000000000..1822496b37069f119060e789f1228cea746e1327 Binary files /dev/null and b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/flash_5s.png differ diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/focus_box.png b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/focus_box.png new file mode 100644 index 0000000000000000000000000000000000000000..7a5d9b649e08465de0e8e8883a9a37d7c3bb9f36 Binary files /dev/null and b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/focus_box.png differ diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/foreground.png b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..97014d3e10e5ff511409c378cd4255713aecd85f Binary files /dev/null and b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/foreground.png differ diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/layered_image.json b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/startIcon.png b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..205ad8b5a8a42e8762fbe4899b8e5e31ce822b8b Binary files /dev/null and b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/startIcon.png differ diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/toggle_position.png b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/toggle_position.png new file mode 100644 index 0000000000000000000000000000000000000000..0843141ece2b81e910903dcd2b153f37ee4bf612 Binary files /dev/null and b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/media/toggle_position.png differ diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/profile/backup_config.json b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000000000000000000000000000000000000..78f40ae7c494d71e2482278f359ec790ca73471a --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/profile/main_pages.json b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000000000000000000000000000000000..1898d94f58d6128ab712be2c68acc7c98e9ab9ce --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/dark/element/color.json b/CameraKit/RotationDisplayCamera/entry/src/main/resources/dark/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..79b11c2747aec33e710fd3a7b2b3c94dd9965499 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/resources/dark/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/en_US/element/string.json b/CameraKit/RotationDisplayCamera/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..84573dc11ed7df2080447c4a2dcd36c44ccccd45 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,140 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "CustomCamera" + }, + { + "name": "permission_CAMERA", + "value": "For camera operations" + }, + { + "name": "reason_microphone", + "value": "For camera video recording" + }, + { + "name": "reason_media_location", + "value": "For scenarios where the camera retrieves media information" + }, + { + "name": "reason_write_imagevideo", + "value": "For reading and writing media files with the camera" + }, + { + "name": "reason_read_imagevideo", + "value": "For camera media file read/write" + }, + { + "name": "permission_SENSOR", + "value": "For sensors" + }, + { + "name": "recording", + "value": "Recording in progress" + }, + { + "name": "paused", + "value": "Paused" + }, + { + "name": "wakeup_text", + "value": "Tap the screen to wake up the camera" + }, + { + "name": "photo", + "value": "Photo" + }, + { + "name": "video", + "value": "Video" + }, + { + "name": "preview_rate", + "value": "Preview frame rate%d" + }, + { + "name": "delay", + "value": "Time-lapse%d" + }, + { + "name": "delay_close", + "value": "Off time-lapse" + }, + { + "name": "moving_open", + "value": "Live photo mode is on" + }, + { + "name": "moving_close", + "value": "Live photo mode is off" + }, + { + "name": "photo_single", + "value": "Single-segment shooting" + }, + { + "name": "photo_double", + "value": "Dual-segment shooting" + }, + { + "name": "stabilization_enable", + "value": "Video stabilization is on" + }, + { + "name": "stabilization_disabled", + "value": "Video stabilization is off" + }, + { + "name": "grid_line_open", + "value": "Grid lines are enabled." + }, + { + "name": "grid_line_close", + "value": "Grid lines are disabled." + }, + { + "name": "level_open", + "value": "The spirit level is enabled" + }, + { + "name": "level_close", + "value": "The spirit level is disabled" + }, + { + "name": "flash_auto", + "value": "Flash is in auto mode" + }, + { + "name": "flash_close", + "value": "The flash is off." + }, + { + "name": "flash_always", + "value": "The flash is always on" + }, + { + "name": "flash_open", + "value": "The flash is on" + }, + { + "name": "preview_image_open", + "value": "Dual-channel preview has been enabled" + }, + { + "name": "preview_image_close", + "value": "Dual-channel preview has been disabled" + }, + { + "name": "permission_LOCATION", + "value": "For location" + } + ] +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/main/resources/zh_CN/element/string.json b/CameraKit/RotationDisplayCamera/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..fc4e395ef928cabd2525ab4101ac5ee6fdd018ca --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,140 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "CameraKitSample" + }, + { + "name": "permission_CAMERA", + "value": "用于相机操作" + }, + { + "name": "reason_microphone", + "value": "用于相机录像场景" + }, + { + "name": "reason_media_location", + "value": "用于相机获取媒体信息场景" + }, + { + "name": "reason_write_imagevideo", + "value": "用于相机读写媒体文件" + }, + { + "name": "reason_read_imagevideo", + "value": "用于相机读写媒体文件" + }, + { + "name": "permission_SENSOR", + "value": "用于传感器" + }, + { + "name": "recording", + "value": "录制中" + }, + { + "name": "paused", + "value": "已暂停" + }, + { + "name": "wakeup_text", + "value": "点击屏幕唤醒相机" + }, + { + "name": "photo", + "value": "拍照" + }, + { + "name": "video", + "value": "录像" + }, + { + "name": "preview_rate", + "value": "预览帧率%d" + }, + { + "name": "delay", + "value": "延时拍照%d" + }, + { + "name": "delay_close", + "value": "延时拍照关闭" + }, + { + "name": "moving_open", + "value": "动态拍照已开启" + }, + { + "name": "moving_close", + "value": "动态拍照已关闭" + }, + { + "name": "photo_single", + "value": "单段拍照" + }, + { + "name": "photo_double", + "value": "双段拍照" + }, + { + "name": "stabilization_enable", + "value": "录像防抖已开启" + }, + { + "name": "stabilization_disabled", + "value": "录像防抖已关闭" + }, + { + "name": "grid_line_open", + "value": "网格线已开启" + }, + { + "name": "grid_line_close", + "value": "网格线已关闭" + }, + { + "name": "level_open", + "value": "水平仪已开启" + }, + { + "name": "level_close", + "value": "水平仪已关闭" + }, + { + "name": "flash_auto", + "value": "闪关灯已自动" + }, + { + "name": "flash_close", + "value": "闪光灯已关闭" + }, + { + "name": "flash_always", + "value": "闪光灯已常亮" + }, + { + "name": "flash_open", + "value": "闪光灯已开启" + }, + { + "name": "preview_image_open", + "value": "双路预览已开启" + }, + { + "name": "preview_image_close", + "value": "双路预览已关闭" + }, + { + "name": "permission_LOCATION", + "value": "用于地理位置" + } + ] +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/ohosTest/ets/test/Ability.test.ets b/CameraKit/RotationDisplayCamera/entry/src/ohosTest/ets/test/Ability.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..b5f95e765dbee7c650b4f75791aa58eb1009de34 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/ohosTest/ets/test/Ability.test.ets @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2025 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 AbilityDelegatorRegistry from '@ohos.app.ability.abilityDelegatorRegistry'; +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import Base from '@ohos.base'; +import abilityAccessCtrl from '@ohos.abilityAccessCtrl'; +import { PermissionRequestResult } from '@ohos.abilityAccessCtrl'; +import { Permissions } from '@kit.AbilityKit'; +import { Driver, ON, MatchPattern, Component } from '@ohos.UiTest'; +import fs from '@ohos.file.fs'; +import Want from '@ohos.app.ability.Want'; + +const TAG = 'abilityTest'; +const domain: number = 0x0000; + +export default function abilityTest() { + describe('ActsAbilityTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }) + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }) + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }) + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }) + it('captureTest', 0, async () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + try { + let want: Want = { + bundleName: 'com.example.CameraKit', + abilityName: 'EntryAbility' + }; + let abilityDelegator = AbilityDelegatorRegistry.getAbilityDelegator(); + abilityDelegator.startAbility(want, (err: Base.BusinessError) => { + hilog.info(domain, TAG, 'StartAbility get err ' + JSON.stringify(err)); + // expect(err).assertNull(); + }) + + console.info('%{public}s', ' beforeAll 4'); + let driver = Driver.create(); + await driver.delayMs(1000); + let permissionButton: Component | null = null; + for (let i = 0; i < 3; i++) { + permissionButton = await driver.waitForComponent(ON.text('允许', MatchPattern.EQUALS), 1000); + console.info('captureTest btn1' + JSON.stringify(permissionButton)); + if (permissionButton != null) { + await permissionButton.click(); + console.info('captureTest after permissionButton.click '); + await driver.delayMs(300); // 等待下一个权限请求弹窗出现 + } + } + permissionButton = await driver.findComponent(ON.text('仅使用期间允许', MatchPattern.EQUALS)); + await permissionButton.click(); + await driver.delayMs(300); // 等待下一个权限请求弹窗出现 + await driver.delayMs(300); + console.info('%{public}s', ' beforeAll 6'); + let btn1 = await driver.findComponent(ON.text('拍照', MatchPattern.EQUALS)); + console.info('captureTest btn1' + JSON.stringify(btn1)); + await btn1.click(); + await driver.delayMs(300); + console.info('%{public}s', ' beforeAll 8'); + let btn2 = await driver.findComponent(ON.type('Column').id('Capture')); + console.info('captureTest btn2' + JSON.stringify(btn2)); + await btn2.click(); + console.info('%{public}s', ' beforeAll 10'); + await driver.delayMs(3000); + expect(btn2 == null).assertFalse(); + await driver.delayMs(1000); + } catch (err) { + console.info(' captureTest failed, err: ' + err); + } + }) + + it('recordTest', 0, async () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + try { + let driver = Driver.create(); + await driver.delayMs(300); + let btn1 = await driver.findComponent(ON.text('录像', MatchPattern.EQUALS)); + console.info('recordTest btn1' + JSON.stringify(btn1)); + await btn1.click(); + await driver.delayMs(300); + console.info('%{public}s', ' recordTest 8'); + let btn2 = await driver.findComponent(ON.type('Column').id('VideoStartButton')); + console.info('recordTest btn2' + JSON.stringify(btn2)); + await btn2.click(); + console.info('%{public}s', ' recordTest 10'); + await driver.delayMs(3000); + let btn3 = await driver.findComponent(ON.type('Column').id('VideoStopButton')); + console.info('recordTest btn3' + JSON.stringify(btn3)); + expect(btn3 == null).assertFalse(); + await btn3.click(); + await driver.delayMs(500); + let btn4 = await driver.findComponent(ON.type('Image').id('Thumbnail')); + console.info('recordTest btn4' + JSON.stringify(btn4)); + await btn4.click(); + await driver.delayMs(500); + } catch (err) { + console.info(' recordTest failed, err: ' + err); + } + }) + }) +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/ohosTest/ets/test/List.test.ets b/CameraKit/RotationDisplayCamera/entry/src/ohosTest/ets/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..6068cbde604813d2160e3a433ac144904769ee3a --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/ohosTest/ets/test/List.test.ets @@ -0,0 +1,6 @@ +import abilityTest from './Ability.test'; + + +export default function testsuite() { + abilityTest(); +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/ohosTest/module.json5 b/CameraKit/RotationDisplayCamera/entry/src/ohosTest/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..69026872775eebd0844900b225c411959ae5608b --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/ohosTest/module.json5 @@ -0,0 +1,12 @@ +{ + "module": { + "name": "entry_test", + "type": "feature", + "deviceTypes": [ + "default", + "tablet" + ], + "deliveryWithInstall": true, + "installationFree": false + } +} diff --git a/CameraKit/RotationDisplayCamera/entry/src/test/List.test.ets b/CameraKit/RotationDisplayCamera/entry/src/test/List.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..bb5b5c3731e283dd507c847560ee59bde477bbc7 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/test/List.test.ets @@ -0,0 +1,5 @@ +import localUnitTest from './LocalUnit.test'; + +export default function testsuite() { + localUnitTest(); +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/entry/src/test/LocalUnit.test.ets b/CameraKit/RotationDisplayCamera/entry/src/test/LocalUnit.test.ets new file mode 100644 index 0000000000000000000000000000000000000000..165fc1615ee8618b4cb6a622f144a9a707eee99f --- /dev/null +++ b/CameraKit/RotationDisplayCamera/entry/src/test/LocalUnit.test.ets @@ -0,0 +1,33 @@ +import { describe, beforeAll, beforeEach, afterEach, afterAll, it, expect } from '@ohos/hypium'; + +export default function localUnitTest() { + describe('localUnitTest', () => { + // Defines a test suite. Two parameters are supported: test suite name and test suite function. + beforeAll(() => { + // Presets an action, which is performed only once before all test cases of the test suite start. + // This API supports only one parameter: preset action function. + }); + beforeEach(() => { + // Presets an action, which is performed before each unit test case starts. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: preset action function. + }); + afterEach(() => { + // Presets a clear action, which is performed after each unit test case ends. + // The number of execution times is the same as the number of test cases defined by **it**. + // This API supports only one parameter: clear action function. + }); + afterAll(() => { + // Presets a clear action, which is performed after all test cases of the test suite end. + // This API supports only one parameter: clear action function. + }); + it('assertContain', 0, () => { + // Defines a test case. This API supports three parameters: test case name, filter parameter, and test case function. + let a = 'abc'; + let b = 'b'; + // Defines a variety of assertion methods, which are used to declare expected boolean conditions. + expect(a).assertContain(b); + expect(a).assertEqual(a); + }); + }); +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/hvigor/hvigor-config.json5 b/CameraKit/RotationDisplayCamera/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..5bebc9755447385d82ce4138f54d991b1f85f348 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/hvigor/hvigor-config.json5 @@ -0,0 +1,22 @@ +{ + "modelVersion": "5.0.5", + "dependencies": { + }, + "execution": { + // "analyze": "normal", /* Define the build analyze mode. Value: [ "normal" | "advanced" | false ]. Default: "normal" */ + // "daemon": true, /* Enable daemon compilation. Value: [ true | false ]. Default: true */ + // "incremental": true, /* Enable incremental compilation. Value: [ true | false ]. Default: true */ + // "parallel": true, /* Enable parallel compilation. Value: [ true | false ]. Default: true */ + // "typeCheck": false, /* Enable typeCheck. Value: [ true | false ]. Default: false */ + }, + "logging": { + // "level": "info" /* Define the log level. Value: [ "debug" | "info" | "warn" | "error" ]. Default: "info" */ + }, + "debugging": { + // "stacktrace": false /* Disable stacktrace compilation. Value: [ true | false ]. Default: false */ + }, + "nodeOptions": { + // "maxOldSpaceSize": 8192 /* Enable nodeOptions maxOldSpaceSize compilation. Unit M. Used for the daemon process. Default: 8192*/ + // "exposeGC": true /* Enable to trigger garbage collection explicitly. Default: true*/ + } +} diff --git a/CameraKit/RotationDisplayCamera/hvigorfile.ts b/CameraKit/RotationDisplayCamera/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3cb9f1a87a81687554a76283af8df27d8bda775 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/hvigorfile.ts @@ -0,0 +1,6 @@ +import { appTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: appTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/CameraKit/RotationDisplayCamera/oh-package-lock.json5 b/CameraKit/RotationDisplayCamera/oh-package-lock.json5 new file mode 100644 index 0000000000000000000000000000000000000000..989ca2a577b8eba3c143b631a3421b435eedf89d --- /dev/null +++ b/CameraKit/RotationDisplayCamera/oh-package-lock.json5 @@ -0,0 +1,21 @@ +{ + "meta": { + "stableOrder": true, + "enableUnifiedLockfile": false + }, + "lockfileVersion": 3, + "ATTENTION": "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.", + "specifiers": { + "@ohos/hypium@1.0.18": "@ohos/hypium@1.0.18" + }, + "packages": { + "@ohos/hypium@1.0.18": { + "name": "@ohos/hypium", + "version": "1.0.18", + "integrity": "sha512-UMwbHlaJ1lEaJkDoQC/NH8XztcKysKWjzwgybDHULNKP8undtPtJbFww69GhTp8EG6+NO1uXimRBUl6sBI2nWw==", + "resolved": "https://cmc.centralrepo.rnd.huawei.com/artifactory/api/npm/product_npm/@ohos/hypium/-/@ohos/hypium-1.0.18.tgz", + "shasum": "cdbaf30436c45461a0c527da1aab0e916c7e1195", + "registryType": "npm" + } + } +} \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/oh-package.json5 b/CameraKit/RotationDisplayCamera/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..114bdf65f7a3a7157b50109f09e7a0edcf44e832 --- /dev/null +++ b/CameraKit/RotationDisplayCamera/oh-package.json5 @@ -0,0 +1,9 @@ +{ + "modelVersion": "5.0.5", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": { + "@ohos/hypium": "1.0.18" + } +} diff --git a/CameraKit/RotationDisplayCamera/ohosTest.md b/CameraKit/RotationDisplayCamera/ohosTest.md new file mode 100644 index 0000000000000000000000000000000000000000..51427de99f632413cf3132bba101f088fb59207c --- /dev/null +++ b/CameraKit/RotationDisplayCamera/ohosTest.md @@ -0,0 +1,9 @@ +# CustomCamera-master测试用例归档 + +## 用例表 + +| 测试功能 | 预置条件 | 输入 | 预期输出 | 是否自动 | 测试结果 | +|---------------|-----------|---------------------------------------------------------------|-----------|------|------| +| 拉起应用 | 设备正常运行 | | 成功拉起应用 | 是 | Pass | +| 拍照功能 | 进入示例应用 | 1. 点击拍照
2. 点击拍照按钮 | 拍照图片保存至图库 | 是 | Pass | +| 录制功能 | 进入示例应用 | 1. 点击录像
2. 点击录像开始按钮
3. 一段时间后点击录像停止按钮 | 录像视频保存至图库 | 是 | Pass | \ No newline at end of file diff --git a/CameraKit/RotationDisplayCamera/screenshots/devices/camera.png b/CameraKit/RotationDisplayCamera/screenshots/devices/camera.png new file mode 100644 index 0000000000000000000000000000000000000000..a1aae63b08f301c7dcaa364233e294d6cf7e16cd Binary files /dev/null and b/CameraKit/RotationDisplayCamera/screenshots/devices/camera.png differ