diff --git a/DealStrideSolution/.gitignore b/DealStrideSolution/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d2ff20141ceed86d87c0ea5d99481973005bab2b --- /dev/null +++ b/DealStrideSolution/.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/DealStrideSolution/AppScope/app.json5 b/DealStrideSolution/AppScope/app.json5 new file mode 100644 index 0000000000000000000000000000000000000000..34c7d09d65358b07c868833c811a81f43c1304ac --- /dev/null +++ b/DealStrideSolution/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.example.dealstridesolution", + "vendor": "example", + "versionCode": 1000000, + "versionName": "1.0.0", + "icon": "$media:app_icon", + "label": "$string:app_name" + } +} diff --git a/DealStrideSolution/AppScope/resources/base/element/string.json b/DealStrideSolution/AppScope/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..602690f54472bdcbe69b90d250aaf804392c05a2 --- /dev/null +++ b/DealStrideSolution/AppScope/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "DealStrideSolution" + } + ] +} diff --git a/DealStrideSolution/AppScope/resources/base/media/app_icon.png b/DealStrideSolution/AppScope/resources/base/media/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a39445dc87828b76fed6d2ec470dd455c45319e3 Binary files /dev/null and b/DealStrideSolution/AppScope/resources/base/media/app_icon.png differ diff --git a/DealStrideSolution/LICENSE b/DealStrideSolution/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..338e5b0bc22082e0ffcc7121c2ed3897a3ddccb0 --- /dev/null +++ b/DealStrideSolution/LICENSE @@ -0,0 +1,78 @@ + Copyright (c) 2024 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/DealStrideSolution/README.md b/DealStrideSolution/README.md new file mode 100644 index 0000000000000000000000000000000000000000..de18509c1a1a7729755f116c46223af2cfda489d --- /dev/null +++ b/DealStrideSolution/README.md @@ -0,0 +1,167 @@ +# 处理stride解决相机预览花屏问题 + +## 介绍 +开发者在使用相机服务时,如果仅用于预览流展示,通常使用XComponent组件实现,如果需要获取每帧图像做二次处理(例如获取每帧图像完成二维码识别或人脸识别场景),可以通过ImageReceiver中imageArrival事件监听预览流每帧数据,解析图像内容。在解析图像内容时,如果未考虑stride,直接通过使用width*height读取图像内容去解析图像,会导致相机预览异常,从而出现相机预览花屏的现象。 +当开发者获取预览流每帧图像buffer后,若发现图片内容出现花屏堆叠状,出现“相机预览花屏”现象,此时需要排查解析每帧图像,当预览流图像stride与width不一致时,需要对stride进行无效像素处理。 + +## 效果 + +| 正例一 | 正例二 | +|----------------------------------------------------------|----------------------------------------------------------| +| | | + +## 使用说明 +1、父页面点击"反例:未处理stride"按钮,跳转子页面NoDealStride,子页面的预览界面展示相机预览流,出现花屏现象。 + +1、父页面点击"处理stride:方案一"按钮,跳转子页面DealStrideOne,子页面的预览界面展示相机预览流,无明显花屏现象。 + +2、父页面点击"处理stride:方案二"按钮,跳转子页面DealStrideTwo,子页面的预览界面展示相机预览流,无明显花屏现象。 + +### 目录结构 + +``` +├──entry/src/main/ets/ +│ ├──common +│ │ ├──CommonConstants.ets // 通用常量 +│ │ └──Constants.ets // 业务涉及的常量 +│ ├──entryability +│ │ └──EntryAbility.ets // Ability的生命周期回调内容 +│ ├──entrybackupability +│ │ └──EntryBackipAbility.ets // 自定义应用数据转换和迁移模板类 +│ ├──pages +│ │ ├──Index.ets // 应用入口页 +│ │ ├──PageOne.ets // 预览流展示页(正例方案一) +│ │ ├──PageThree.ets // 反例预览流展示页(反例) +│ │ └──PageTwo.ets // 预览流展示页(正例方案二) +│ └──utils +│ ├──CameraServiceOne.ets // 相机服务类(正例方案一) +│ ├──CameraServiceThree.ets // 相机服务类(反例) +│ ├──CameraServiceTwo.ets // 相机服务类(正例方案二) +│ └──Logger.ets // 日志工具类 +└──entry/src/main/resources // 应用静态资源目录 +``` + +## 具体实现 +以一种高频的用户使用场景为例,应用需要定义一个1080*1080分辨率的预览流图像,此时的stride在相关平台的返回值为1088,此时需要对stride进行处理,处理无效像素后解析出正确的像素数据,传给Image组件送显,避免出现预览流花屏。 +1. 应用通过image.ImageReceiver注册imageArrival图像回调方法,获取每帧图像数据实例image.Image,应用通过定义一个width为1080*height为1080分辨率的预览流直接创建pixelMap,此时获取到的stride的值为1088,需将处理stride后的buffer传给Image送显,预览流正常展示。 +2. 处理stride有两种方式,详见方案一二。 +### 方案一 +开发者使用width,height,stride三个值,处理相机预览流数据。 +分两种情况: +- 当stride和width相等时,按宽读取buffer不影响结果; +- 当stride和width不等时,将相机返回的预览流数据即component.byteBuffer中的数据去除掉stride, +此时将component.byteBuffer中的数据去除掉stride,拷贝得到新的dstArr数据进行数据处理,将处理后的dstArr数组buffer,通过width和height直接创建pixelMap, 并存储到全局变量stridePixel中,传给Image送显。 +以下为关键示例代码: +```typescript +function onImageArrival(receiver: image.ImageReceiver): void { + receiver.on('imageArrival', () => { + receiver.readNextImage((err: BusinessError, nextImage: image.Image) => { + if (err || nextImage === undefined) { + Logger.error(TAG, `requestPermissionsFromUser call Failed! error: ${err.code}`); + return; + } + nextImage.getComponent(image.ComponentType.JPEG, async (err, component: image.Component) => { + let width = 1080; // Application create preview stream resolution corresponding to the width + let height = 1080; // Application create preview stream resolution corresponding to the height + let stride = component.rowStride; // Get stride by using component.rowStride + Logger.info(TAG, `receiver getComponent width:${width} height:${height} stride:${stride}`); + // Positive example: Case 1.stride and width are equal. Reading buffer by width does not affect the result. + if (stride === width) { + let pixelMap = await image.createPixelMap(component.byteBuffer, { + size: { height: height, width: width }, + srcPixelFormat: image.PixelMapFormat.NV21, + }) + AppStorage.setOrCreate('stridePixel', pixelMap); + } else { + // Positive example: Case 2.When width and stride are not equal, + // At this time, the camera returned preview stream data component.byteBuffer to remove stride, + // copy the new dstArr data, data processing to other do not support stride interface processing. + const dstBufferSize = width * height * 1.5; // Create a dstBufferSize space of width * height * 1.5. This is NV21 data format. + const dstArr = new Uint8Array(dstBufferSize); // Store the buffer after the stride is removed. + // For each line of data read, the camera supports an even width and height profile, which does not involve rounding. + for (let j = 0; j < height * 1.5; j++) { // Loop each row of dstArr data. + // Copy the first width bytes of each line of data from component.byteBuffer into dstArr (remove invalid pixels and get exactly an eight-byte array space of width*height per line). + const srcBuf = new Uint8Array(component.byteBuffer, j * stride, width); // The buffer returned by component.byteBuffer traverses each line, starting at the top, with width bytes cut off each line. + dstArr.set(srcBuf, j * width); // Store the width*height data in dstArr. + } + let pixelMap = await image.createPixelMap(dstArr.buffer, { // The processed dstArr array buffer creates pixelMap directly by width and height, and stores it in the global variable stridePixel and passes it to Image for display. + size: { height: height, width: width }, + srcPixelFormat: image.PixelMapFormat.NV21, + }) + AppStorage.setOrCreate('stridePixel', pixelMap); + } + nextImage.release(); + }) + }); + }) +} +``` + +### 方案二 +开发者使用width,height,stride三个值,处理相机预览流数据。 +分两种情况: +- 当stride和width相等时,与正例一情况一致,此处不再赘述。 +- 当stride和width不等时,如果应用想使用byteBuffer预览流数据创建pixelMap直接显示,可以根据stride*height字节的大小先创建pixelMap,然后调用 +PixelMap的cropSync方法裁剪掉多余的像素,从而正确处理stride,解决预览流花屏问题。 +以下为关键示例代码: +```typescript +function onImageArrival(receiver: image.ImageReceiver): void { + receiver.on('imageArrival', () => { + receiver.readNextImage((err: BusinessError, nextImage: image.Image) => { + if (err || nextImage === undefined) { + Logger.error(TAG, `requestPermissionsFromUser call Failed! error: ${err.code}`); + return; + } + if (nextImage) { + nextImage.getComponent(image.ComponentType.JPEG, async (err, component: image.Component) => { + let width = 1080; // Application create preview stream resolution corresponding to the width + let height = 1080; // Application create preview stream resolution corresponding to the height + let stride = component.rowStride; // Get stride by using component.rowStride + Logger.info(TAG, `receiver getComponent width:${width} height:${height} stride:${stride}`); + // stride and width are equal. Reading buffer by width does not affect the result + if (stride === width) { + let pixelMap = await image.createPixelMap(component.byteBuffer, { + size: { height: height, width: width }, + srcPixelFormat: image.PixelMapFormat.NV21, + }) + AppStorage.setOrCreate('stridePixel', pixelMap); + } else { + let pixelMap = await image.createPixelMap(component.byteBuffer, { + // Positive example: 1. width transmission stride when creating PixelMap. + size: { height: height, width: stride }, + srcPixelFormat: 8, + }) + // 2. then call the cropSync method of PixelMap to crop out the excess pixels. + pixelMap.cropSync({ + size: { width: width, height: height }, + x: 0, + y: 0 + }) // Crop the image according to the size entered, starting with (0,0), crop the area of width*height bytes. + let pixelBefore: PixelMap | undefined = AppStorage.get('stridePixel'); + await pixelBefore?.release(); + AppStorage.setOrCreate('stridePixel', pixelMap); + } + nextImage.release(); + }) + } + }); + }) +} +``` +### 相关权限 + +允许应用使用相机:ohos.permission.CAMERA + +### 依赖 + +不涉及。 + +### 约束与限制 + +1.本示例仅支持标准系统上运行,支持设备:华为手机。 + +2.HarmonyOS系统:HarmonyOS 5.0.0 Release及以上。 + +3.DevEco Studio版本:DevEco Studio 5.0.0 Release及以上。 + +4.HarmonyOS SDK版本:HarmonyOS 5.0.0 Release SDK及以上。 \ No newline at end of file diff --git a/DealStrideSolution/build-profile.json5 b/DealStrideSolution/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..1e69556b3411622cb2e87a87389653bb34f1b148 --- /dev/null +++ b/DealStrideSolution/build-profile.json5 @@ -0,0 +1,41 @@ +{ + "app": { + "signingConfigs": [], + "products": [ + { + "name": "default", + "signingConfig": "default", + "compatibleSdkVersion": "5.0.0(12)", + "runtimeOS": "HarmonyOS", + "buildOption": { + "strictMode": { + "caseSensitiveCheck": true, + "useNormalizedOHMUrl": true + } + } + } + ], + "buildModeSet": [ + { + "name": "debug", + }, + { + "name": "release" + } + ] + }, + "modules": [ + { + "name": "entry", + "srcPath": "./entry", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/DealStrideSolution/entry/.gitignore b/DealStrideSolution/entry/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..e2713a2779c5a3e0eb879efe6115455592caeea5 --- /dev/null +++ b/DealStrideSolution/entry/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/DealStrideSolution/entry/build-profile.json5 b/DealStrideSolution/entry/build-profile.json5 new file mode 100644 index 0000000000000000000000000000000000000000..4d611879c7913fb0610c686e2399258ab3a6dad1 --- /dev/null +++ b/DealStrideSolution/entry/build-profile.json5 @@ -0,0 +1,28 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": false, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + } + }, + ], + "targets": [ + { + "name": "default" + }, + { + "name": "ohosTest", + } + ] +} \ No newline at end of file diff --git a/DealStrideSolution/entry/hvigorfile.ts b/DealStrideSolution/entry/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6edcd90486dd5a853cf7d34c8647f08414ca7a3 --- /dev/null +++ b/DealStrideSolution/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/DealStrideSolution/entry/obfuscation-rules.txt b/DealStrideSolution/entry/obfuscation-rules.txt new file mode 100644 index 0000000000000000000000000000000000000000..272efb6ca3f240859091bbbfc7c5802d52793b0b --- /dev/null +++ b/DealStrideSolution/entry/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# Define project specific obfuscation rules here. +# You can include the obfuscation configuration files in the current module's build-profile.json5. +# +# For more details, see +# https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/source-obfuscation-V5 + +# Obfuscation options: +# -disable-obfuscation: disable all obfuscations +# -enable-property-obfuscation: obfuscate the property names +# -enable-toplevel-obfuscation: obfuscate the names in the global scope +# -compact: remove unnecessary blank spaces and all line feeds +# -remove-log: remove all console.* statements +# -print-namecache: print the name cache that contains the mapping from the old names to new names +# -apply-namecache: reuse the given cache file + +# Keep options: +# -keep-property-name: specifies property names that you want to keep +# -keep-global-name: specifies names that you want to keep in the global scope + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/DealStrideSolution/entry/oh-package.json5 b/DealStrideSolution/entry/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..248c3b7541a589682a250f86a6d3ecf7414d2d6a --- /dev/null +++ b/DealStrideSolution/entry/oh-package.json5 @@ -0,0 +1,10 @@ +{ + "name": "entry", + "version": "1.0.0", + "description": "Please describe the basic information.", + "main": "", + "author": "", + "license": "", + "dependencies": {} +} + diff --git a/DealStrideSolution/entry/src/main/ets/common/CommonConstants.ets b/DealStrideSolution/entry/src/main/ets/common/CommonConstants.ets new file mode 100644 index 0000000000000000000000000000000000000000..b0a8d0f3bc03ae06d9958044cbeb56113cb86cf6 --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/common/CommonConstants.ets @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2024 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. + */ + +/** + * Common constants for common component. + */ +export class CommonConstants { + // percent + static readonly FULL_PERCENT: string = '100%'; +} diff --git a/DealStrideSolution/entry/src/main/ets/common/Constants.ts b/DealStrideSolution/entry/src/main/ets/common/Constants.ts new file mode 100644 index 0000000000000000000000000000000000000000..03df7571d5a91b53aa0db128f7ea8c9798272b81 --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/common/Constants.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 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 class Constants { + /** + * Surface width in xComponent. + */ + static readonly X_COMPONENT_SURFACE_WIDTH = 1080; // 1080*1080 stride->1088 + /** + * Surface height in xComponent. + */ + static readonly X_COMPONENT_SURFACE_HEIGHT = 1080; +}; \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/ets/entryability/EntryAbility.ets b/DealStrideSolution/entry/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..dd56c05a43816f0b1132df308769780b026caffd --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2024 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, UIAbility } from '@kit.AbilityKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; +import { window } from '@kit.ArkUI'; +import { BusinessError } from '@kit.BasicServicesKit'; +import Logger from '../utils/Logger'; + +const TAG = 'EntryAbility'; + +export default class EntryAbility extends UIAbility { + onCreate(): void { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate'); + } + + onDestroy(): void { + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy'); + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + // Main window is created, set main page for this ability + Logger.info(TAG, 'Ability onWindowStageCreate'); + windowStage.getMainWindow().then((win: window.Window): void => { + win.setWindowLayoutFullScreen(true).then((): void => { + }); + win.setWindowSystemBarProperties({ + // Navigation bar color + navigationBarColor: '#F1F3F5', + // Status bar color + statusBarColor: '#F1F3F5' + }) + }); + + windowStage.loadContent('pages/Index', (err, data) => { + if (err.code) { + Logger.info(TAG, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); + return; + } + Logger.info(TAG, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); + // Get camera permissions + let atManager = abilityAccessCtrl.createAtManager(); + atManager.requestPermissionsFromUser(this.context, [ + 'ohos.permission.CAMERA', + ]).then((): void => { + Logger.info(TAG, 'request Permissions success!'); + }).catch((error: BusinessError): void => { + Logger.info(TAG, `requestPermissionsFromUser call Failed! error: ${error.code}`); + }); + }); + } + + onWindowStageDestroy(): void { + // Main window is destroyed, release UI related resources + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy'); + } + + onForeground(): void { + // Ability has brought to foreground + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground'); + } + + onBackground(): void { + // Ability has back to background + hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground'); + } +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets b/DealStrideSolution/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets new file mode 100644 index 0000000000000000000000000000000000000000..b9aeb2679294b37bbf46d72c11d065cc5caf0eb7 --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024 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'; +import { BackupExtensionAbility, BundleVersion } from '@kit.CoreFileKit'; + +export default class EntryBackupAbility extends BackupExtensionAbility { + async onBackup() { + hilog.info(0x0000, 'testTag', 'onBackup ok'); + } + + async onRestore(bundleVersion: BundleVersion) { + hilog.info(0x0000, 'testTag', 'onRestore ok %{public}s', JSON.stringify(bundleVersion)); + } +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/ets/pages/Index.ets b/DealStrideSolution/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000000000000000000000000000000000000..baad38861c93bd83288a54215fd343485c84f839 --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 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. + */ + +@Entry +@Component +struct Index { + @State childNavStack: NavPathStack = new NavPathStack(); + @State isShowStridePixel: boolean = false; + @State imageWidth: number = 1080; + @State imageHeight: number = 1080; + + @Builder + ParentTitle() { + Text($r('app.string.total_title')) + .width('100%') + .fontSize(30) + .fontWeight(700) + .lineHeight(40) + .padding({ + left: 12, + right: 20 + }) + .textAlign(TextAlign.Start) + } + + build() { + Navigation(this.childNavStack) { + Column() { + this.ParentTitle() + Column() { + Button($r('app.string.btn_three'), { stateEffect: true, type: ButtonType.Capsule }) + .width('100%') + .height(40) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 12 }) + .onClick(() => { + this.childNavStack.pushPath({ name: 'pageThree' }); + }) + Button($r('app.string.btn_one'), { stateEffect: true, type: ButtonType.Capsule }) + .width('100%') + .height(40) + .fontWeight(FontWeight.Bold) + .margin({ bottom: 12 }) + .onClick(() => { + this.childNavStack.pushPath({ name: 'pageOne' }); + }) + Button($r('app.string.btn_two'), { stateEffect: true, type: ButtonType.Capsule }) + .width('100%') + .height(40) + .fontWeight(FontWeight.Bold) + .onClick(() => { + this.childNavStack.pushPath({ name: 'pageTwo' }); + }) + } + .width('100%') + .padding({ + left: 16, + right: 16 + }) + } + .width('100%') + .height('100%') + .padding({ + top: 92, + bottom: 44 + }) + .justifyContent(FlexAlign.SpaceBetween) + } + .backgroundColor('#F1F1F1') + .height('100%') + .hideTitleBar(true) + .mode(NavigationMode.Stack) + } +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/ets/pages/PageOne.ets b/DealStrideSolution/entry/src/main/ets/pages/PageOne.ets new file mode 100644 index 0000000000000000000000000000000000000000..35631465a6b4203591bb1bb05ab37839d0f8deaa --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/pages/PageOne.ets @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024 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 CameraService from '../utils/CameraServiceOne'; +import { CommonConstants } from '../common/CommonConstants'; +import { promptAction } from '@kit.ArkUI'; + +@Builder +export function PageOneBuilder() { + PageOne() +} + +@Component +export struct PageOne { + pathStack: NavPathStack = new NavPathStack(); + @State name: string = 'pageOne'; + @State isShowStridePixel: boolean = false; + @StorageLink('stridePixel') @Watch('onStridePixel') stridePixel: image.PixelMap | undefined = undefined; + @State imageWidth: number = 1080; + @State imageHeight: number = 1080; + @StorageLink('previewRotation') previewRotate: number = 0; + + onStridePixel(): void { + this.isShowStridePixel = true; + } + + aboutToAppear(): void { + CameraService.initCamera(0); + } + + aboutToDisappear(): void { + CameraService.releaseCamera(); + } + + @Builder + navDestinationTitleImg(res:Resource,fun:()=>void) { + Image(res) + .padding(8) + .width(40) + .height(40) + .borderRadius('50%') + .colorFilter('#171717') + .backgroundColor('#e6e8e9') + .onClick(() => { + fun(); + }) + } + + @Builder + navDestinationTitle() { + Row() { + this.navDestinationTitleImg($r('app.media.back'), () => { + this.pathStack.pop(); + }) + + Text($r('app.string.title_one')) + .fontSize(20) + .fontWeight(FontWeight.Bold) + .textAlign(TextAlign.Start) + .lineHeight(26) + .padding({ left: 8 }) + .fontColor(Color.Black) + .layoutWeight(1) + + this.navDestinationTitleImg($r('app.media.more'), () => { + promptAction.showToast({ message: $r('app.string.only_show_for_ux') }) + }) + } + .width('100%') + .height(56) + .margin({ top: 36 }) + .padding({ + left: 16, + right: 16 + }) + .backgroundColor('#F1F3F5') + } + + build() { + NavDestination() { + this.navDestinationTitle() + Column() { + if (this.isShowStridePixel) { + Image(this.stridePixel) + .width(px2vp(this.imageWidth)) + .height(px2vp(this.imageHeight)) + .margin({ top: 150 }) + .rotate({ + z: 0.5, + angle: this.previewRotate + }) + } + Blank() + PublishView() + } + .justifyContent(FlexAlign.Center) + .height('90%') + .width('100%') + } + .backgroundColor(Color.White) + .hideTitleBar(true) + .onBackPressed(() => { + this.pathStack.pop(); + return true; + }) + .onReady((context: NavDestinationContext) => { + this.pathStack = context.pathStack; + }) + } +} + +// Share components at the bottom of the page +@Component +export struct PublishView { + build() { + Flex({ justifyContent: FlexAlign.SpaceBetween }) { + Image($r('app.media.share')) + .width(24) + .height(24) + Image($r('app.media.heart')) + .width(24) + .height(24) + Image($r('app.media.square_and_pencil')) + .width(24) + .height(24) + Image($r('app.media.trash')) + .width(24) + .height(24) + } + .backgroundColor('#F1F3F5') + .width(CommonConstants.FULL_PERCENT) + .height(86) + .padding({ + bottom: 44, + top: 16, + left: 18, + right: 18 + }) + .onClick(() => { + promptAction.showToast({ message: $r('app.string.only_show_for_ux') }); + }) + } +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/ets/pages/PageThree.ets b/DealStrideSolution/entry/src/main/ets/pages/PageThree.ets new file mode 100644 index 0000000000000000000000000000000000000000..cbced5a3d71976955aa86919b75b43d79e2c8a25 --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/pages/PageThree.ets @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024 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 CameraService from '../utils/CameraServiceThree'; +import { CommonConstants } from '../common/CommonConstants'; +import { promptAction } from '@kit.ArkUI'; + +@Builder +export function PageThreeBuilder() { + PageThree() +} + +@Component +export struct PageThree { + pathStack: NavPathStack = new NavPathStack(); + @State name: string = 'pageOne'; + @State isShowStridePixel: boolean = false; + @StorageLink('stridePixel') @Watch('onStridePixel') stridePixel: image.PixelMap | undefined = undefined; + @State imageWidth: number = 1080; + @State imageHeight: number = 1080; + @StorageLink('previewRotation') previewRotate: number = 0; + + onStridePixel(): void { + this.isShowStridePixel = true; + } + + aboutToAppear(): void { + CameraService.initCamera(0); + } + + aboutToDisappear(): void { + CameraService.releaseCamera(); + } + + @Builder + navDestinationTitleImg(res:Resource,fun:()=>void) { + Image(res) + .padding(8) + .width(40) + .height(40) + .borderRadius('50%') + .colorFilter('#171717') + .backgroundColor('#e6e8e9') + .onClick(() => { + fun(); + }) + } + + @Builder + navDestinationTitle() { + Row() { + this.navDestinationTitleImg($r('app.media.back'), () => { + this.pathStack.pop(); + }) + + Text($r('app.string.title_three')) + .fontSize(20) + .fontWeight(FontWeight.Bold) + .textAlign(TextAlign.Start) + .lineHeight(26) + .padding({ left: 8 }) + .fontColor(Color.Black) + .layoutWeight(1) + + this.navDestinationTitleImg($r('app.media.more'), () => { + promptAction.showToast({ message: $r('app.string.only_show_for_ux') }) + }) + } + .width('100%') + .height(56) + .margin({ top: 36 }) + .padding({ + left: 16, + right: 16 + }) + .backgroundColor('#F1F3F5') + } + + build() { + NavDestination() { + this.navDestinationTitle() + Column() { + if (this.isShowStridePixel) { + Image(this.stridePixel) + .width(px2vp(this.imageWidth)) + .height(px2vp(this.imageHeight)) + .margin({ top: 150 }) + .rotate({ + z: 0.5, + angle: this.previewRotate + }) + } + Blank() + PublishView() + } + .justifyContent(FlexAlign.Center) + .height('90%') + .width('100%') + } + .backgroundColor(Color.White) + .hideTitleBar(true) + .onBackPressed(() => { + this.pathStack.pop(); + return true; + }) + .onReady((context: NavDestinationContext) => { + this.pathStack = context.pathStack; + }) + } +} + +// Share components at the bottom of the page +@Component +export struct PublishView { + build() { + Flex({ justifyContent: FlexAlign.SpaceBetween }) { + Image($r('app.media.share')) + .width(24) + .height(24) + Image($r('app.media.heart')) + .width(24) + .height(24) + Image($r('app.media.square_and_pencil')) + .width(24) + .height(24) + Image($r('app.media.trash')) + .width(24) + .height(24) + } + .backgroundColor('#F1F3F5') + .width(CommonConstants.FULL_PERCENT) + .height(86) + .padding({ + bottom: 44, + top: 16, + left: 18, + right: 18 + }) + .onClick(() => { + promptAction.showToast({ message: $r('app.string.only_show_for_ux') }); + }) + } +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/ets/pages/PageTwo.ets b/DealStrideSolution/entry/src/main/ets/pages/PageTwo.ets new file mode 100644 index 0000000000000000000000000000000000000000..1362df013096288587c6fe61d13d6216044fc6a6 --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/pages/PageTwo.ets @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2024 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 CameraService from '../utils/CameraServiceTwo'; +import { CommonConstants } from '../common/CommonConstants'; +import { promptAction } from '@kit.ArkUI'; + +@Builder +export function PageTwoBuilder() { + PageTwo() +} + +@Component +export struct PageTwo { + pathStack: NavPathStack = new NavPathStack(); + @State name: string = 'pageOne'; + @State isShowStridePixel: boolean = false; + @StorageLink('stridePixel') @Watch('onStridePixel') stridePixel: image.PixelMap | undefined = undefined; + @State imageWidth: number = 1080; + @State imageHeight: number = 1080; + @StorageLink('previewRotation') previewRotate: number = 0; + + onStridePixel(): void { + this.isShowStridePixel = true; + } + + aboutToAppear(): void { + CameraService.initCamera(0); + } + + aboutToDisappear(): void { + CameraService.releaseCamera(); + } + + @Builder + navDestinationTitleImg(res:Resource,fun:()=>void) { + Image(res) + .padding(8) + .width(40) + .height(40) + .borderRadius('50%') + .colorFilter('#171717') + .backgroundColor('#e6e8e9') + .onClick(() => { + fun(); + }) + } + + @Builder + navDestinationTitle() { + Row() { + this.navDestinationTitleImg($r('app.media.back'), () => { + this.pathStack.pop(); + }) + + Text($r('app.string.title_two')) + .fontSize(20) + .fontWeight(FontWeight.Bold) + .textAlign(TextAlign.Start) + .lineHeight(26) + .padding({ left: 8 }) + .fontColor(Color.Black) + .layoutWeight(1) + + this.navDestinationTitleImg($r('app.media.more'), () => { + promptAction.showToast({ message: $r('app.string.only_show_for_ux') }) + }) + } + .width('100%') + .height(56) + .margin({ top: 36 }) + .padding({ + left: 16, + right: 16 + }) + .backgroundColor('#F1F3F5') + } + + build() { + NavDestination() { + this.navDestinationTitle() + Column() { + if (this.isShowStridePixel) { + Image(this.stridePixel) + .width(px2vp(this.imageWidth)) + .height(px2vp(this.imageHeight)) + .margin({ top: 150 }) + .rotate({ + z: 0.5, + angle: this.previewRotate + }) + } + Blank() + PublishView() + } + .justifyContent(FlexAlign.Center) + .height('90%') + .width('100%') + } + .backgroundColor(Color.White) + .hideTitleBar(true) + .onBackPressed(() => { + this.pathStack.pop(); + return true; + }) + .onReady((context: NavDestinationContext) => { + this.pathStack = context.pathStack; + }) + } +} + +// Share components at the bottom of the page +@Component +export struct PublishView { + build() { + Flex({ justifyContent: FlexAlign.SpaceBetween }) { + Image($r('app.media.share')) + .width(24) + .height(24) + Image($r('app.media.heart')) + .width(24) + .height(24) + Image($r('app.media.square_and_pencil')) + .width(24) + .height(24) + Image($r('app.media.trash')) + .width(24) + .height(24) + } + .backgroundColor('#F1F3F5') + .width(CommonConstants.FULL_PERCENT) + .height(86) + .padding({ + bottom: 44, + top: 16, + left: 18, + right: 18 + }) + .onClick(() => { + promptAction.showToast({ message: $r('app.string.only_show_for_ux') }); + }) + } +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/ets/utils/CameraServiceOne.ets b/DealStrideSolution/entry/src/main/ets/utils/CameraServiceOne.ets new file mode 100644 index 0000000000000000000000000000000000000000..76ab4bd3c0e30d1aafedcb04821afe3ef6937754 --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/utils/CameraServiceOne.ets @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2024 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 { BusinessError } from '@kit.BasicServicesKit'; +import { Constants } from '../common/Constants'; +import { camera } from '@kit.CameraKit'; +import { image } from '@kit.ImageKit'; +import { JSON } from '@kit.ArkTS'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import Logger from './Logger'; + +const TAG: string = 'CameraService'; + +class CameraService { + private cameraManager: camera.CameraManager | undefined = undefined; + private cameras: Array | Array = []; + private cameraInput: camera.CameraInput | undefined = undefined; + private previewOutput: camera.PreviewOutput | undefined = undefined; + private session: camera.PhotoSession | camera.VideoSession | undefined = undefined; + handlePhotoAssetCb: (photoAsset: photoAccessHelper.PhotoAsset) => void = () => {}; + private curCameraDevice: camera.CameraDevice | undefined = undefined; + private receiver: image.ImageReceiver | undefined = undefined; + frameStartFlag: number = 0; + // One of the recommended preview resolutions + private previewProfileObj: camera.Profile = { + format: 1003, + size: { + width: Constants.X_COMPONENT_SURFACE_WIDTH, + height: Constants.X_COMPONENT_SURFACE_HEIGHT + } + }; + surfaceId: string = ''; + + onImageArrival(receiver: image.ImageReceiver): void { + receiver.on('imageArrival', () => { + receiver.readNextImage((err: BusinessError, nextImage: image.Image) => { + if (err || nextImage === undefined) { + Logger.error(TAG, `requestPermissionsFromUser call Failed! error: ${err.code}`); + return; + } + if (nextImage) { + nextImage.getComponent(image.ComponentType.JPEG, + async (err, component: image.Component) => { + let width = 1080; // Application create preview stream resolution corresponding to the width + let height = 1080; // Application create preview stream resolution corresponding to the height + let stride = component.rowStride; // Get stride by using component.rowStride + Logger.info(TAG, `receiver getComponent width:${width} height:${height} stride:${stride}`); + // Positive example: Case 1.stride and width are equal. Reading buffer by width does not affect the result. + if (stride === width) { + let pixelMap = await image.createPixelMap(component.byteBuffer, { + size: { height: height, width: width }, + srcPixelFormat: image.PixelMapFormat.NV21, + }) + AppStorage.setOrCreate('stridePixel', pixelMap); + } else { + // Positive example: Case 2.When width and stride are not equal, + // At this time, the camera returned preview stream data component.byteBuffer to remove stride, + // copy the new dstArr data, data processing to other do not support stride interface processing. + const dstBufferSize = width * height * 1.5; // Create a dstBufferSize space of width * height * 1.5. This is NV21 data format. + const dstArr = new Uint8Array(dstBufferSize); // Store the buffer after the stride is removed. + // For each line of data read, the camera supports an even width and height profile, which does not involve rounding. + for (let j = 0; j < height * 1.5; j++) { // Loop each row of dstArr data. + // Copy the first width bytes of each line of data from component.byteBuffer into dstArr + // (remove invalid pixels and get exactly an eight-byte array space of width*height per line). + const srcBuf = new Uint8Array(component.byteBuffer, j * stride, + width); // The buffer returned by component.byteBuffer traverses each line, starting at the top, with width bytes cut off each line. + dstArr.set(srcBuf, j * width); // Store the width*height data in dstArr. + } + let pixelMap = await image.createPixelMap(dstArr.buffer, { + // The processed dstArr array buffer creates pixelMap directly by width and height, + // and stores it in the global variable stridePixel and passes it to Image for display. + size: { height: height, width: width }, + srcPixelFormat: image.PixelMapFormat.NV21, + }) + AppStorage.setOrCreate('stridePixel', pixelMap); + } + nextImage.release(); + }) + } + }); + }) + } + + getPreviewProfile(cameraOutputCapability: camera.CameraOutputCapability): camera.Profile | undefined { + let previewProfiles = cameraOutputCapability.previewProfiles; + if (previewProfiles.length < 1) { + return undefined; + } + let index = previewProfiles.findIndex((previewProfile: camera.Profile) => { + return previewProfile.size.width === this.previewProfileObj.size.width && + previewProfile.size.height === this.previewProfileObj.size.height && + previewProfile.format === this.previewProfileObj.format; + }); + if (index === -1) { + return undefined; + } + return previewProfiles[index]; + } + + /** + * Initializes the camera function + * @param surfaceId - Surface ID + * @param cameraDeviceIndex - Camera equipment index + * @returns No return value + */ + async initCamera(cameraDeviceIndex: number): Promise { + Logger.debug(TAG, `initCamera cameraDeviceIndex: ${cameraDeviceIndex}`); + try { + await this.releaseCamera(); + // Get the Camera Manager instance + this.cameraManager = this.getCameraManagerFn(); + if (this.cameraManager === undefined) { + Logger.error(TAG, 'cameraManager is undefined'); + return; + } + this.cameras = this.getSupportedCamerasFn(this.cameraManager); + this.curCameraDevice = this.cameras[cameraDeviceIndex]; + if (this.curCameraDevice === undefined) { + Logger.error(TAG, 'Failed to create the camera input.'); + return; + } + // Create the cameraInput output object + this.cameraInput = this.createCameraInputFn(this.cameraManager, this.curCameraDevice); + if (this.cameraInput === undefined) { + Logger.error(TAG, 'Failed to create the camera input.'); + return; + } + // Turn on the camera + let isOpenSuccess = await this.cameraInputOpenFn(this.cameraInput); + if (!isOpenSuccess) { + Logger.error(TAG, 'Failed to open the camera.'); + return; + } + + // Choose a profile with a different stride and width + let previewProfile: camera.Profile = { + format: camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP, + size: { + width: Constants.X_COMPONENT_SURFACE_WIDTH, + height: Constants.X_COMPONENT_SURFACE_HEIGHT + } + }; + + let size: image.Size = { + width: Constants.X_COMPONENT_SURFACE_WIDTH, + height: Constants.X_COMPONENT_SURFACE_HEIGHT + } + this.receiver = image.createImageReceiver(size, image.ImageFormat.JPEG, 8); + let surfaceId: string = await this.receiver.getReceivingSurfaceId(); + this.previewOutput = this.createPreviewOutputFn(this.cameraManager, previewProfile, surfaceId); + this.onImageArrival(this.receiver); + + if (this.previewOutput === undefined) { + Logger.error(TAG, 'Failed to create the preview stream.'); + return; + } + + // Session flow + await this.sessionFlowFn(this.cameraManager, this.cameraInput, this.previewOutput); + } catch (error) { + Logger.error(TAG, `initCamera fail: ${JSON.stringify(error)}`); + } + } + + getPreviewRotation() { + let previewRotation: camera.ImageRotation | undefined = camera.ImageRotation.ROTATION_0; + previewRotation = this.previewOutput?.getPreviewRotation(previewRotation); + AppStorage.set('previewRotation', previewRotation); + } + + /** + * Release the session and related parameters + */ + async releaseCamera(): Promise { + Logger.info(TAG, 'releaseCamera is called'); + try { + await this.receiver?.release(); + } catch (err) { + Logger.error(TAG, `imageReceiver release fail: error: ${JSON.stringify(err)}`); + } + try { + await this.previewOutput?.release(); + } catch (err) { + Logger.error(TAG, `previewOutput release fail: error: ${JSON.stringify(err)}`); + } finally { + this.previewOutput = undefined; + } + try { + await this.session?.release(); + } catch (err) { + Logger.error(TAG, `captureSession release fail: error: ${JSON.stringify(err)}`); + } finally { + this.session = undefined; + } + try { + await this.cameraInput?.close(); + } catch (err) { + Logger.error(TAG, `cameraInput close fail: error: ${JSON.stringify(err)}`); + } finally { + this.cameraInput = undefined; + } + Logger.info(TAG, 'releaseCamera success'); + } + + /** + * Get the Camera Manager instance + */ + getCameraManagerFn(): camera.CameraManager | undefined { + if (this.cameraManager) { + return this.cameraManager; + } + let cameraManager: camera.CameraManager | undefined = undefined; + try { + cameraManager = camera.getCameraManager(getContext(this)); + Logger.info(TAG, `getCameraManager success: ${cameraManager}`); + } catch (error) { + Logger.error(TAG, `getCameraManager failed: ${JSON.stringify(error)}`); + } + return cameraManager; + } + + /** + * Gets the camera device object that supports the specified + */ + getSupportedCamerasFn(cameraManager: camera.CameraManager): Array { + let supportedCameras: Array = []; + try { + supportedCameras = cameraManager.getSupportedCameras(); + Logger.info(TAG, `getSupportedCameras success: ${this.cameras}, length: ${this.cameras?.length}`); + } catch (error) { + Logger.error(TAG, `getSupportedCameras failed: ${JSON.stringify(error)}`); + } + return supportedCameras; + } + + /** + * Create the cameraInput output object + */ + createCameraInputFn(cameraManager: camera.CameraManager, + cameraDevice: camera.CameraDevice): camera.CameraInput | undefined { + Logger.info(TAG, 'createCameraInputFn is called.'); + let cameraInput: camera.CameraInput | undefined = undefined; + try { + cameraInput = cameraManager.createCameraInput(cameraDevice); + Logger.info(TAG, 'createCameraInputFn success'); + } catch (error) { + Logger.error(TAG, `createCameraInputFn failed: ${JSON.stringify(error)}`); + } + return cameraInput; + } + + /** + * Create the previewOutput output object + */ + createPreviewOutputFn(cameraManager: camera.CameraManager, previewProfileObj: camera.Profile, + surfaceId: string): camera.PreviewOutput | undefined { + let previewOutput: camera.PreviewOutput | undefined = undefined; + try { + previewOutput = cameraManager.createPreviewOutput(previewProfileObj, surfaceId); + Logger.info(TAG, `createPreviewOutput success: ${previewOutput}`); + } catch (error) { + Logger.error(TAG, `createPreviewOutput failed: ${JSON.stringify(error)}`); + } + return previewOutput; + } + + /** + * Turn on the camera + */ + async cameraInputOpenFn(cameraInput: camera.CameraInput): Promise { + let isOpenSuccess = false; + try { + await cameraInput.open(); + isOpenSuccess = true; + Logger.info(TAG, 'cameraInput open success'); + } catch (error) { + Logger.error(TAG, `createCameraInput failed : ${JSON.stringify(error)}`); + } + return isOpenSuccess; + } + + /** + * Session flow + */ + async sessionFlowFn(cameraManager: camera.CameraManager, cameraInput: camera.CameraInput, + previewOutput: camera.PreviewOutput | undefined): Promise { + try { + // Create CaptureSession instances + this.session = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession; + this.session.beginConfig(); + this.session.addInput(cameraInput); + this.session.addOutput(previewOutput); + this.getPreviewRotation(); + await this.session.commitConfig(); + await this.session.start(); + Logger.info(TAG, 'sessionFlowFn success'); + } catch (error) { + Logger.error(TAG, `sessionFlowFn fail : ${JSON.stringify(error)}`); + } + } +} + +export default new CameraService(); \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/ets/utils/CameraServiceThree.ets b/DealStrideSolution/entry/src/main/ets/utils/CameraServiceThree.ets new file mode 100644 index 0000000000000000000000000000000000000000..8e0f6f9b290aaec9f214dcf1d610808889d00642 --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/utils/CameraServiceThree.ets @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2024 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 { BusinessError } from '@kit.BasicServicesKit'; +import { Constants } from '../common/Constants'; +import { camera } from '@kit.CameraKit'; +import { image } from '@kit.ImageKit'; +import { JSON } from '@kit.ArkTS'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import Logger from './Logger'; + +const TAG: string = 'CameraService'; + +class CameraService { + private cameraManager: camera.CameraManager | undefined = undefined; + private cameras: Array | Array = []; + private cameraInput: camera.CameraInput | undefined = undefined; + private previewOutput: camera.PreviewOutput | undefined = undefined; + private session: camera.PhotoSession | camera.VideoSession | undefined = undefined; + handlePhotoAssetCb: (photoAsset: photoAccessHelper.PhotoAsset) => void = () => {}; + private curCameraDevice: camera.CameraDevice | undefined = undefined; + private receiver: image.ImageReceiver | undefined = undefined; + frameStartFlag: number = 0; + // One of the recommended preview resolutions + private previewProfileObj: camera.Profile = { + format: 1003, + size: { + width: Constants.X_COMPONENT_SURFACE_WIDTH, + height: Constants.X_COMPONENT_SURFACE_HEIGHT + } + }; + surfaceId: string = ''; + + onImageArrival(receiver: image.ImageReceiver): void { + receiver.on('imageArrival', () => { + receiver.readNextImage((err: BusinessError, nextImage: image.Image) => { + if (err || nextImage === undefined) { + Logger.error(TAG, `requestPermissionsFromUser call Failed! error: ${err.code}`); + return; + } + if (nextImage) { + nextImage.getComponent(image.ComponentType.JPEG, async (_err, component: image.Component) => { + let width = 1080; // Application create preview stream resolution corresponding to the width + let height = 1080; // Application create preview stream resolution corresponding to the height + let pixelMap = await image.createPixelMap(component.byteBuffer, { + size: { + height: height, + width: width + }, + // Counter example:width does not pass the stride value, + // create PixelMap parsing buffer directly according to the width to read each line od data, + // may use invalid pixel data,resulting in preview flowScreen. + srcPixelFormat: image.PixelMapFormat.NV21 + }) + AppStorage.setOrCreate('stridePixel', pixelMap); + nextImage.release(); + }) + } + }); + }) + } + + getPreviewProfile(cameraOutputCapability: camera.CameraOutputCapability): camera.Profile | undefined { + let previewProfiles = cameraOutputCapability.previewProfiles; + if (previewProfiles.length < 1) { + return undefined; + } + let index = previewProfiles.findIndex((previewProfile: camera.Profile) => { + return previewProfile.size.width === this.previewProfileObj.size.width && + previewProfile.size.height === this.previewProfileObj.size.height && + previewProfile.format === this.previewProfileObj.format; + }); + if (index === -1) { + return undefined; + } + return previewProfiles[index]; + } + + /** + * Initializes the camera function + * @param surfaceId - Surface ID + * @param cameraDeviceIndex - Camera equipment index + * @returns No return value + */ + async initCamera(cameraDeviceIndex: number): Promise { + Logger.debug(TAG, `initCamera cameraDeviceIndex: ${cameraDeviceIndex}`); + try { + await this.releaseCamera(); + // Get the Camera Manager instance + this.cameraManager = this.getCameraManagerFn(); + if (this.cameraManager === undefined) { + Logger.error(TAG, 'cameraManager is undefined'); + return; + } + this.cameras = this.getSupportedCamerasFn(this.cameraManager); + this.curCameraDevice = this.cameras[cameraDeviceIndex]; + if (this.curCameraDevice === undefined) { + Logger.error(TAG, 'Failed to create the camera input.'); + return; + } + // Create the cameraInput output object + this.cameraInput = this.createCameraInputFn(this.cameraManager, this.curCameraDevice); + if (this.cameraInput === undefined) { + Logger.error(TAG, 'Failed to create the camera input.'); + return; + } + // Turn on the camera + let isOpenSuccess = await this.cameraInputOpenFn(this.cameraInput); + if (!isOpenSuccess) { + Logger.error(TAG, 'Failed to open the camera.'); + return; + } + + // Choose a profile with a different stride and width + let previewProfile: camera.Profile = { + format: camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP, + size: { + width: Constants.X_COMPONENT_SURFACE_WIDTH, + height: Constants.X_COMPONENT_SURFACE_HEIGHT + } + }; + + let size: image.Size = { + width: Constants.X_COMPONENT_SURFACE_WIDTH, + height: Constants.X_COMPONENT_SURFACE_HEIGHT + } + this.receiver = image.createImageReceiver(size, image.ImageFormat.JPEG, 8); + let surfaceId: string = await this.receiver.getReceivingSurfaceId(); + this.previewOutput = this.createPreviewOutputFn(this.cameraManager, previewProfile, surfaceId); + this.onImageArrival(this.receiver); + + if (this.previewOutput === undefined) { + Logger.error(TAG, 'Failed to create the preview stream.'); + return; + } + + // Session flow + await this.sessionFlowFn(this.cameraManager, this.cameraInput, this.previewOutput); + } catch (error) { + Logger.error(TAG, `initCamera fail: ${JSON.stringify(error)}`); + } + } + + getPreviewRotation() { + let previewRotation: camera.ImageRotation | undefined = camera.ImageRotation.ROTATION_0; + previewRotation = this.previewOutput?.getPreviewRotation(previewRotation); + AppStorage.set('previewRotation', previewRotation) + } + + /** + * Release the session and related parameters + */ + async releaseCamera(): Promise { + Logger.info(TAG, 'releaseCamera is called'); + try { + await this.receiver?.release(); + } catch (err) { + Logger.error(TAG, `imageReceiver release fail: error: ${JSON.stringify(err)}`); + } + try { + await this.previewOutput?.release(); + } catch (err) { + Logger.error(TAG, `previewOutput release fail: error: ${JSON.stringify(err)}`); + } finally { + this.previewOutput = undefined; + } + try { + await this.session?.release(); + } catch (err) { + Logger.error(TAG, `captureSession release fail: error: ${JSON.stringify(err)}`); + } finally { + this.session = undefined; + } + try { + await this.cameraInput?.close(); + } catch (err) { + Logger.error(TAG, `cameraInput close fail: error: ${JSON.stringify(err)}`); + } finally { + this.cameraInput = undefined; + } + Logger.info(TAG, 'releaseCamera success'); + } + + /** + * Get the Camera Manager instance + */ + getCameraManagerFn(): camera.CameraManager | undefined { + if (this.cameraManager) { + return this.cameraManager; + } + let cameraManager: camera.CameraManager | undefined = undefined; + try { + cameraManager = camera.getCameraManager(getContext(this)); + Logger.info(TAG, `getCameraManager success: ${cameraManager}`); + } catch (error) { + Logger.error(TAG, `getCameraManager failed: ${JSON.stringify(error)}`); + } + return cameraManager; + } + + /** + * Gets the camera device object that supports the specified + */ + getSupportedCamerasFn(cameraManager: camera.CameraManager): Array { + let supportedCameras: Array = []; + try { + supportedCameras = cameraManager.getSupportedCameras(); + Logger.info(TAG, `getSupportedCameras success: ${this.cameras}, length: ${this.cameras?.length}`); + } catch (error) { + Logger.error(TAG, `getSupportedCameras failed: ${JSON.stringify(error)}`); + } + return supportedCameras; + } + + /** + * Create the cameraInput output object + */ + createCameraInputFn(cameraManager: camera.CameraManager, + cameraDevice: camera.CameraDevice): camera.CameraInput | undefined { + Logger.info(TAG, 'createCameraInputFn is called.'); + let cameraInput: camera.CameraInput | undefined = undefined; + try { + cameraInput = cameraManager.createCameraInput(cameraDevice); + Logger.info(TAG, 'createCameraInputFn success'); + } catch (error) { + Logger.error(TAG, `createCameraInputFn failed: ${JSON.stringify(error)}`); + } + return cameraInput; + } + + /** + * Create the previewOutput output object + */ + createPreviewOutputFn(cameraManager: camera.CameraManager, previewProfileObj: camera.Profile, + surfaceId: string): camera.PreviewOutput | undefined { + let previewOutput: camera.PreviewOutput | undefined = undefined; + try { + previewOutput = cameraManager.createPreviewOutput(previewProfileObj, surfaceId); + Logger.info(TAG, `createPreviewOutput success: ${previewOutput}`); + } catch (error) { + Logger.error(TAG, `createPreviewOutput failed: ${JSON.stringify(error)}`); + } + return previewOutput; + } + + /** + * Turn on the camera + */ + async cameraInputOpenFn(cameraInput: camera.CameraInput): Promise { + let isOpenSuccess = false; + try { + await cameraInput.open(); + isOpenSuccess = true; + Logger.info(TAG, 'cameraInput open success'); + } catch (error) { + Logger.error(TAG, `createCameraInput failed : ${JSON.stringify(error)}`); + } + return isOpenSuccess; + } + + /** + * Session flow + */ + async sessionFlowFn(cameraManager: camera.CameraManager, cameraInput: camera.CameraInput, + previewOutput: camera.PreviewOutput | undefined): Promise { + try { + // Create CaptureSession instances + this.session = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession; + this.session.beginConfig(); + this.session.addInput(cameraInput); + this.session.addOutput(previewOutput); + this.getPreviewRotation(); + await this.session.commitConfig(); + await this.session.start(); + Logger.info(TAG, 'sessionFlowFn success'); + } catch (error) { + Logger.error(TAG, `sessionFlowFn fail : ${JSON.stringify(error)}`); + } + } +} + +export default new CameraService(); \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/ets/utils/CameraServiceTwo.ets b/DealStrideSolution/entry/src/main/ets/utils/CameraServiceTwo.ets new file mode 100644 index 0000000000000000000000000000000000000000..782596fc9fbfc27ef40b1131e8a34a298fd8b57f --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/utils/CameraServiceTwo.ets @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2024 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 { BusinessError } from '@kit.BasicServicesKit'; +import { Constants } from '../common/Constants'; +import { camera } from '@kit.CameraKit'; +import { image } from '@kit.ImageKit'; +import { JSON } from '@kit.ArkTS'; +import { photoAccessHelper } from '@kit.MediaLibraryKit'; +import Logger from './Logger'; + +const TAG: string = 'CameraService'; + +class CameraService { + private cameraManager: camera.CameraManager | undefined = undefined; + private cameras: Array | Array = []; + private cameraInput: camera.CameraInput | undefined = undefined; + private previewOutput: camera.PreviewOutput | undefined = undefined; + private session: camera.PhotoSession | camera.VideoSession | undefined = undefined; + handlePhotoAssetCb: (photoAsset: photoAccessHelper.PhotoAsset) => void = () => {}; + private curCameraDevice: camera.CameraDevice | undefined = undefined; + private receiver: image.ImageReceiver | undefined = undefined; + frameStartFlag: number = 0; + // One of the recommended preview resolutions + private previewProfileObj: camera.Profile = { + format: 1003, + size: { + width: Constants.X_COMPONENT_SURFACE_WIDTH, + height: Constants.X_COMPONENT_SURFACE_HEIGHT + } + }; + surfaceId: string = ''; + + onImageArrival(receiver: image.ImageReceiver): void { + receiver.on('imageArrival', () => { + receiver.readNextImage((err: BusinessError, nextImage: image.Image) => { + if (err || nextImage === undefined) { + Logger.error(TAG, `requestPermissionsFromUser call Failed! error: ${err.code}`); + return; + } + if (nextImage) { + nextImage.getComponent(image.ComponentType.JPEG, async (_err, component: image.Component) => { + let width = 1080; // Application create preview stream resolution corresponding to the width + let height = 1080; // Application create preview stream resolution corresponding to the height + let stride = component.rowStride; // Get stride by using component.rowStride + Logger.info(TAG, `receiver getComponent width:${width} height:${height} stride:${stride}`); + // stride and width are equal. Reading buffer by width does not affect the result + if (stride === width) { + let pixelMap = await image.createPixelMap(component.byteBuffer, { + size: { height: height, width: width }, + srcPixelFormat: image.PixelMapFormat.NV21, + }) + AppStorage.setOrCreate('stridePixel', pixelMap); + } else { + let pixelMap = await image.createPixelMap(component.byteBuffer, { + // Positive example: 1. width transmission stride when creating PixelMap. + size: { height: height, width: stride }, + srcPixelFormat: 8, + }) + // 2. then call the cropSync method of PixelMap to crop out the excess pixels. + pixelMap.cropSync({ + size: { width: width, height: height }, + x: 0, + y: 0 + }) // Crop the image according to the size entered, starting with (0,0), crop the area of width*height bytes. + let pixelBefore: PixelMap | undefined = AppStorage.get('stridePixel'); + await pixelBefore?.release(); + AppStorage.setOrCreate('stridePixel', pixelMap); + } + nextImage.release(); + }) + } + }); + }) + } + + getPreviewProfile(cameraOutputCapability: camera.CameraOutputCapability): camera.Profile | undefined { + let previewProfiles = cameraOutputCapability.previewProfiles; + if (previewProfiles.length < 1) { + return undefined; + } + let index = previewProfiles.findIndex((previewProfile: camera.Profile) => { + return previewProfile.size.width === this.previewProfileObj.size.width && + previewProfile.size.height === this.previewProfileObj.size.height && + previewProfile.format === this.previewProfileObj.format; + }); + if (index === -1) { + return undefined; + } + return previewProfiles[index]; + } + + /** + * Initializes the camera function + * @param surfaceId - Surface ID + * @param cameraDeviceIndex - Camera equipment index + * @returns No return value + */ + async initCamera(cameraDeviceIndex: number): Promise { + Logger.debug(TAG, `initCamera cameraDeviceIndex: ${cameraDeviceIndex}`); + try { + await this.releaseCamera(); + // Get the Camera Manager instance + this.cameraManager = this.getCameraManagerFn(); + if (this.cameraManager === undefined) { + Logger.error(TAG, 'cameraManager is undefined'); + return; + } + this.cameras = this.getSupportedCamerasFn(this.cameraManager); + this.curCameraDevice = this.cameras[cameraDeviceIndex]; + if (this.curCameraDevice === undefined) { + Logger.error(TAG, 'Failed to create the camera input.'); + return; + } + // Create the cameraInput output object + this.cameraInput = this.createCameraInputFn(this.cameraManager, this.curCameraDevice); + if (this.cameraInput === undefined) { + Logger.error(TAG, 'Failed to create the camera input.'); + return; + } + // Turn on the camera + let isOpenSuccess = await this.cameraInputOpenFn(this.cameraInput); + if (!isOpenSuccess) { + Logger.error(TAG, 'Failed to open the camera.'); + return; + } + + // Choose a profile with a different stride and width + let previewProfile: camera.Profile = { + format: camera.CameraFormat.CAMERA_FORMAT_YUV_420_SP, + size: { + width: Constants.X_COMPONENT_SURFACE_WIDTH, + height: Constants.X_COMPONENT_SURFACE_HEIGHT + } + }; + + let size: image.Size = { + width: Constants.X_COMPONENT_SURFACE_WIDTH, + height: Constants.X_COMPONENT_SURFACE_HEIGHT + } + this.receiver = image.createImageReceiver(size, image.ImageFormat.JPEG, 8); + let surfaceId: string = await this.receiver.getReceivingSurfaceId(); + this.previewOutput = this.createPreviewOutputFn(this.cameraManager, previewProfile, surfaceId); + this.onImageArrival(this.receiver); + + if (this.previewOutput === undefined) { + Logger.error(TAG, 'Failed to create the preview stream.'); + return; + } + + // Session flow + await this.sessionFlowFn(this.cameraManager, this.cameraInput, this.previewOutput); + } catch (error) { + Logger.error(TAG, `initCamera fail: ${JSON.stringify(error)}`); + } + } + + getPreviewRotation() { + let previewRotation: camera.ImageRotation | undefined = camera.ImageRotation.ROTATION_0; + previewRotation = this.previewOutput?.getPreviewRotation(previewRotation); + AppStorage.set('previewRotation', previewRotation) + } + + /** + * Release the session and related parameters + */ + async releaseCamera(): Promise { + Logger.info(TAG, 'releaseCamera is called'); + try { + await this.receiver?.release(); + } catch (err) { + Logger.error(TAG, `imageReceiver release fail: error: ${JSON.stringify(err)}`); + } + try { + await this.previewOutput?.release(); + } catch (err) { + Logger.error(TAG, `previewOutput release fail: error: ${JSON.stringify(err)}`); + } finally { + this.previewOutput = undefined; + } + try { + await this.session?.release(); + } catch (err) { + Logger.error(TAG, `captureSession release fail: error: ${JSON.stringify(err)}`); + } finally { + this.session = undefined; + } + try { + await this.cameraInput?.close(); + } catch (err) { + Logger.error(TAG, `cameraInput close fail: error: ${JSON.stringify(err)}`); + } finally { + this.cameraInput = undefined; + } + Logger.info(TAG, 'releaseCamera success'); + } + + /** + * Get the Camera Manager instance + */ + getCameraManagerFn(): camera.CameraManager | undefined { + if (this.cameraManager) { + return this.cameraManager; + } + let cameraManager: camera.CameraManager | undefined = undefined; + try { + cameraManager = camera.getCameraManager(getContext(this)); + Logger.info(TAG, `getCameraManager success: ${cameraManager}`); + } catch (error) { + Logger.error(TAG, `getCameraManager failed: ${JSON.stringify(error)}`); + } + return cameraManager; + } + + /** + * Gets the camera device object that supports the specified + */ + getSupportedCamerasFn(cameraManager: camera.CameraManager): Array { + let supportedCameras: Array = []; + try { + supportedCameras = cameraManager.getSupportedCameras(); + Logger.info(TAG, `getSupportedCameras success: ${this.cameras}, length: ${this.cameras?.length}`); + } catch (error) { + Logger.error(TAG, `getSupportedCameras failed: ${JSON.stringify(error)}`); + } + return supportedCameras; + } + + /** + * Create the cameraInput output object + */ + createCameraInputFn(cameraManager: camera.CameraManager, + cameraDevice: camera.CameraDevice): camera.CameraInput | undefined { + Logger.info(TAG, 'createCameraInputFn is called.'); + let cameraInput: camera.CameraInput | undefined = undefined; + try { + cameraInput = cameraManager.createCameraInput(cameraDevice); + Logger.info(TAG, 'createCameraInputFn success'); + } catch (error) { + Logger.error(TAG, `createCameraInputFn failed: ${JSON.stringify(error)}`); + } + return cameraInput; + } + + /** + * Create the previewOutput output object + */ + createPreviewOutputFn(cameraManager: camera.CameraManager, previewProfileObj: camera.Profile, + surfaceId: string): camera.PreviewOutput | undefined { + let previewOutput: camera.PreviewOutput | undefined = undefined; + try { + previewOutput = cameraManager.createPreviewOutput(previewProfileObj, surfaceId); + Logger.info(TAG, `createPreviewOutput success: ${previewOutput}`); + } catch (error) { + Logger.error(TAG, `createPreviewOutput failed: ${JSON.stringify(error)}`); + } + return previewOutput; + } + + /** + * Turn on the camera + */ + async cameraInputOpenFn(cameraInput: camera.CameraInput): Promise { + let isOpenSuccess = false; + try { + await cameraInput.open(); + isOpenSuccess = true; + Logger.info(TAG, 'cameraInput open success'); + } catch (error) { + Logger.error(TAG, `createCameraInput failed : ${JSON.stringify(error)}`); + } + return isOpenSuccess; + } + + /** + * Session flow + */ + async sessionFlowFn(cameraManager: camera.CameraManager, cameraInput: camera.CameraInput, + previewOutput: camera.PreviewOutput | undefined): Promise { + try { + // Create CaptureSession instances + this.session = cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO) as camera.PhotoSession; + this.session.beginConfig(); + this.session.addInput(cameraInput); + this.session.addOutput(previewOutput); + this.getPreviewRotation(); + await this.session.commitConfig(); + await this.session.start(); + Logger.info(TAG, 'sessionFlowFn success'); + } catch (error) { + Logger.error(TAG, `sessionFlowFn fail : ${JSON.stringify(error)}`); + } + } +} + +export default new CameraService(); \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/ets/utils/Logger.ets b/DealStrideSolution/entry/src/main/ets/utils/Logger.ets new file mode 100644 index 0000000000000000000000000000000000000000..9e7ae52b27eb0a3070f7946c57d0ce84540d29cd --- /dev/null +++ b/DealStrideSolution/entry/src/main/ets/utils/Logger.ets @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2024 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'; + +const TAG = 'DealStrideSolution'; + +class Logger { + private domain: number; + private prefix: string; + private format: string = '%{public}s, %{public}s'; + + constructor(prefix: string) { + this.prefix = prefix; + this.domain = 0xFF00; + } + + debug(...args: string[]): void { + hilog.debug(this.domain, this.prefix, this.format, args); + } + + info(...args: string[]): void { + hilog.info(this.domain, this.prefix, this.format, args); + } + + warn(...args: string[]): void { + hilog.warn(this.domain, this.prefix, this.format, args); + } + + error(...args: string[]): void { + hilog.error(this.domain, this.prefix, this.format, args); + } +} + +export default new Logger(TAG); \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/module.json5 b/DealStrideSolution/entry/src/main/module.json5 new file mode 100644 index 0000000000000000000000000000000000000000..e6005930d4010027278082fb626f91d48c9ba708 --- /dev/null +++ b/DealStrideSolution/entry/src/main/module.json5 @@ -0,0 +1,63 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "routerMap": "$profile:router_map", + "deviceTypes": [ + "phone" + ], + "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" + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "EntryBackupAbility", + "srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets", + "type": "backup", + "exported": false, + "metadata": [ + { + "name": "ohos.extension.backup", + "resource": "$profile:backup_config" + } + ], + } + ], + "requestPermissions": [ + { + "name": "ohos.permission.CAMERA", + "reason": "$string:camera_reason", + "usedScene": { + "abilities": [ + "EntryAbility" + ], + "when":"always" + } + } + ] + } +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/resources/base/element/color.json b/DealStrideSolution/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000000000000000000000000000000000000..637168799c46368b68dd5ea07b3eaefb32a63d17 --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/element/color.json @@ -0,0 +1,12 @@ +{ + "color": [ + { + "name": "container_nested_color_blank", + "value": "#000000" + }, + { + "name": "start_window_background", + "value": "#000000" + } + ] +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/resources/base/element/string.json b/DealStrideSolution/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..91a4e63204a1d3c39651b075f610acbb75ddc760 --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/element/string.json @@ -0,0 +1,52 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "DealStrideSolution" + }, + { + "name": "camera_reason", + "value": "This permission is required for using the camera" + }, + { + "name": "only_show_for_ux", + "value": "only show for ux" + }, + { + "name": "title_one", + "value": "DealStrideOne" + }, + { + "name": "title_two", + "value": "DealStrideTwo" + }, + { + "name": "title_three", + "value": "NoDealStride" + }, + { + "name": "total_title", + "value": "Camera preview flower screen solution" + }, + { + "name": "btn_one", + "value": "Dealing with stride: Option One" + }, + { + "name": "btn_two", + "value": "Dealing with stride: Option two" + }, + { + "name": "btn_three", + "value": "Counter example: stride not processed" + } + ] +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/resources/base/media/back.svg b/DealStrideSolution/entry/src/main/resources/base/media/back.svg new file mode 100644 index 0000000000000000000000000000000000000000..1e9405ce617d5dca35385a994b5ce7e384f78dd1 --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/media/back.svg @@ -0,0 +1,13 @@ + + + Public/ic_public_arrow_left + + + + + + + + + + \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/resources/base/media/background.png b/DealStrideSolution/entry/src/main/resources/base/media/background.png new file mode 100644 index 0000000000000000000000000000000000000000..f939c9fa8cc8914832e602198745f592a0dfa34d Binary files /dev/null and b/DealStrideSolution/entry/src/main/resources/base/media/background.png differ diff --git a/DealStrideSolution/entry/src/main/resources/base/media/foreground.png b/DealStrideSolution/entry/src/main/resources/base/media/foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..4483ddad1f079e1089d685bd204ee1cfe1d01902 Binary files /dev/null and b/DealStrideSolution/entry/src/main/resources/base/media/foreground.png differ diff --git a/DealStrideSolution/entry/src/main/resources/base/media/heart.svg b/DealStrideSolution/entry/src/main/resources/base/media/heart.svg new file mode 100644 index 0000000000000000000000000000000000000000..dfca62e0258e2280626b24976cb300674fdb89cc --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/media/heart.svg @@ -0,0 +1,16 @@ + + + Created with Pixso. + + + + + + + + + + + + + diff --git a/DealStrideSolution/entry/src/main/resources/base/media/layered_image.json b/DealStrideSolution/entry/src/main/resources/base/media/layered_image.json new file mode 100644 index 0000000000000000000000000000000000000000..fb49920440fb4d246c82f9ada275e26123a2136a --- /dev/null +++ b/DealStrideSolution/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/DealStrideSolution/entry/src/main/resources/base/media/more.svg b/DealStrideSolution/entry/src/main/resources/base/media/more.svg new file mode 100644 index 0000000000000000000000000000000000000000..5a1760a1b79fd1bb24585408566b1eb35de2f73d --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/media/more.svg @@ -0,0 +1,16 @@ + + + Created with Pixso. + + + + + + + + + + + + + diff --git a/DealStrideSolution/entry/src/main/resources/base/media/share.svg b/DealStrideSolution/entry/src/main/resources/base/media/share.svg new file mode 100644 index 0000000000000000000000000000000000000000..866f067c9f29db02e395f2420edc183c5e3e56d8 --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/media/share.svg @@ -0,0 +1,17 @@ + + + Created with Pixso. + + + + + + + + + + + + + + diff --git a/DealStrideSolution/entry/src/main/resources/base/media/square_and_pencil.svg b/DealStrideSolution/entry/src/main/resources/base/media/square_and_pencil.svg new file mode 100644 index 0000000000000000000000000000000000000000..2ac5de2e073ea1d3d91191dddc7d2975accdf260 --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/media/square_and_pencil.svg @@ -0,0 +1,23 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + + diff --git a/DealStrideSolution/entry/src/main/resources/base/media/startIcon.png b/DealStrideSolution/entry/src/main/resources/base/media/startIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..205ad8b5a8a42e8762fbe4899b8e5e31ce822b8b Binary files /dev/null and b/DealStrideSolution/entry/src/main/resources/base/media/startIcon.png differ diff --git a/DealStrideSolution/entry/src/main/resources/base/media/trash.svg b/DealStrideSolution/entry/src/main/resources/base/media/trash.svg new file mode 100644 index 0000000000000000000000000000000000000000..682c28ab66163f58f6cdebf51f985c70ae2e3a9b --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/media/trash.svg @@ -0,0 +1,16 @@ + + + Created with Pixso. + + + + + + + + + + + + + diff --git a/DealStrideSolution/entry/src/main/resources/base/profile/backup_config.json b/DealStrideSolution/entry/src/main/resources/base/profile/backup_config.json new file mode 100644 index 0000000000000000000000000000000000000000..78f40ae7c494d71e2482278f359ec790ca73471a --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/profile/backup_config.json @@ -0,0 +1,3 @@ +{ + "allowToBackupRestore": true +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/resources/base/profile/main_pages.json b/DealStrideSolution/entry/src/main/resources/base/profile/main_pages.json new file mode 100644 index 0000000000000000000000000000000000000000..1898d94f58d6128ab712be2c68acc7c98e9ab9ce --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/DealStrideSolution/entry/src/main/resources/base/profile/router_map.json b/DealStrideSolution/entry/src/main/resources/base/profile/router_map.json new file mode 100644 index 0000000000000000000000000000000000000000..9eea5b71f8595d54c227b05c4310f8eaafb45f7c --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/base/profile/router_map.json @@ -0,0 +1,28 @@ +{ + "routerMap": [ + { + "name": "pageOne", + "pageSourceFile": "src/main/ets/pages/PageOne.ets", + "buildFunction": "PageOneBuilder", + "data": { + "description": "this is pageOne" + } + }, + { + "name": "pageTwo", + "pageSourceFile": "src/main/ets/pages/PageTwo.ets", + "buildFunction": "PageTwoBuilder", + "data": { + "description": "this is pageTwo" + } + }, + { + "name": "pageThree", + "pageSourceFile": "src/main/ets/pages/PageThree.ets", + "buildFunction": "PageThreeBuilder", + "data": { + "description": "this is pageThree" + } + } + ] +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/resources/en_US/element/string.json b/DealStrideSolution/entry/src/main/resources/en_US/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..91a4e63204a1d3c39651b075f610acbb75ddc760 --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/en_US/element/string.json @@ -0,0 +1,52 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "DealStrideSolution" + }, + { + "name": "camera_reason", + "value": "This permission is required for using the camera" + }, + { + "name": "only_show_for_ux", + "value": "only show for ux" + }, + { + "name": "title_one", + "value": "DealStrideOne" + }, + { + "name": "title_two", + "value": "DealStrideTwo" + }, + { + "name": "title_three", + "value": "NoDealStride" + }, + { + "name": "total_title", + "value": "Camera preview flower screen solution" + }, + { + "name": "btn_one", + "value": "Dealing with stride: Option One" + }, + { + "name": "btn_two", + "value": "Dealing with stride: Option two" + }, + { + "name": "btn_three", + "value": "Counter example: stride not processed" + } + ] +} \ No newline at end of file diff --git a/DealStrideSolution/entry/src/main/resources/zh_CN/element/string.json b/DealStrideSolution/entry/src/main/resources/zh_CN/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..99ff3198636263e318852e9637961fc33bd14ed4 --- /dev/null +++ b/DealStrideSolution/entry/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,48 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "module description" + }, + { + "name": "EntryAbility_desc", + "value": "description" + }, + { + "name": "EntryAbility_label", + "value": "DealStrideSolution" + }, + { + "name": "camera_reason", + "value": "This permission is required for using the camera" + }, + { + "name": "only_show_for_ux", + "value": "仅做ux展示" + }, + { + "name": "title_one", + "value": "DealStrideOne" + }, + { + "name": "title_two", + "value": "DealStrideTwo" + }, + { + "name": "total_title", + "value": "相机预览花屏解决方案" + }, + { + "name": "btn_one", + "value": "处理stride:方案一" + }, + { + "name": "btn_two", + "value": "处理stride:方案二" + }, + { + "name": "btn_three", + "value": "反例:未处理stride" + } + ] +} \ No newline at end of file diff --git a/DealStrideSolution/hvigor/hvigor-config.json5 b/DealStrideSolution/hvigor/hvigor-config.json5 new file mode 100644 index 0000000000000000000000000000000000000000..06b2783670a348f95533b352c1ceda909a842bbc --- /dev/null +++ b/DealStrideSolution/hvigor/hvigor-config.json5 @@ -0,0 +1,22 @@ +{ + "modelVersion": "5.0.0", + "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/DealStrideSolution/hvigorfile.ts b/DealStrideSolution/hvigorfile.ts new file mode 100644 index 0000000000000000000000000000000000000000..f3cb9f1a87a81687554a76283af8df27d8bda775 --- /dev/null +++ b/DealStrideSolution/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/DealStrideSolution/oh-package.json5 b/DealStrideSolution/oh-package.json5 new file mode 100644 index 0000000000000000000000000000000000000000..0cec98259052cc175d04dbcf61780de4bc9785f4 --- /dev/null +++ b/DealStrideSolution/oh-package.json5 @@ -0,0 +1,7 @@ +{ + "modelVersion": "5.0.0", + "description": "Please describe the basic information.", + "dependencies": { + }, + "devDependencies": {} +} diff --git a/DealStrideSolution/screenshots/devices/zhengli1.gif b/DealStrideSolution/screenshots/devices/zhengli1.gif new file mode 100644 index 0000000000000000000000000000000000000000..16c78d199867e45311b6c7024ca3c6c7261ea3da Binary files /dev/null and b/DealStrideSolution/screenshots/devices/zhengli1.gif differ diff --git a/DealStrideSolution/screenshots/devices/zhengli2.gif b/DealStrideSolution/screenshots/devices/zhengli2.gif new file mode 100644 index 0000000000000000000000000000000000000000..c3af99b24a32e628ebc4b7d4441b9824da261f73 Binary files /dev/null and b/DealStrideSolution/screenshots/devices/zhengli2.gif differ diff --git a/README.md b/README.md index 54359cedaf29802a0a5b267668ff3c209c91caa9..fbd0ac847f2c71db63d4a18a0f6dd5bf06da3f68 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ ## 目录 +* [DealStrideSolution: 处理stride解决相机预览花屏问题](DealStrideSolution) * [AppDataSecurity: 应用数据安全](AppDataSecurity) * [AppPrivacyProtection:应用隐私保护](AppPrivacyProtection) * [AvoidTimeComsume:主线程耗时操作优化指导](AvoidTimeComsume)