文档管理中心
FAQ媒体开发拍照和图片相机开发(Camera)不拉起相机实现无感拍照

不拉起相机实现无感拍照

问题现象

教育类应用需要在考试界面定时拍摄,以监测答题者是否有作弊行为,但是因为不想打断考试过程,所以想要在不拉起预览界面的情况下完成拍照,请问是否有可行的方案?

背景知识

  • 在开发相机应用时,需要先申请相关权限。
  • 双路预览(ArkTS):双路预览,即应用可同时使用两路预览流,一路用于在屏幕上显示,一路用于图像处理等其他操作,提升处理效率。本文主要使用ImageReceiver的那一路预览流,移除掉XComponent那一路送显的预览流。

解决方案

业务场景要求不拉起相机实现无感拍照,其实主要是不能拉起XComponent预览界面,因此可以在双路预览方案的基础上,移除XComponent那一路送显的流,仅通过ImageReceiver那一路流去获取并处理相机拍摄到的帧数据。

示例代码如下:

收起
自动换行
深色代码主题
复制
  1. import { camera } from '@kit.CameraKit';
  2. import { image } from '@kit.ImageKit';
  3. import { BusinessError } from '@kit.BasicServicesKit';
  4. import { fileIo as fs } from '@kit.CoreFileKit';
  5. import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit';
  6. import { hilog } from '@kit.PerformanceAnalysisKit';
  7. interface CameraResources {
  8. videoOutput?: camera.VideoOutput;
  9. cameraInput?: camera.CameraInput;
  10. previewOutput?: camera.PreviewOutput;
  11. session?: camera.VideoSession;
  12. }
  13. const DOMAIN: number = 0x0000;
  14. const TAG: string = '[CameraDemo]';
  15. @Entry
  16. @Component
  17. struct Index {
  18. private imageReceiver: image.ImageReceiver | undefined = undefined;
  19. private imageReceiverSurfaceId: string = '';
  20. private imageWidth: number = 1080;
  21. private cameraManager: camera.CameraManager | undefined = undefined;
  22. private cameras: Array<camera.CameraDevice> | undefined = [];
  23. private uiContext: UIContext = this.getUIContext();
  24. private context: Context | undefined = this.uiContext.getHostContext();
  25. private cameraPermission: Permissions = 'ohos.permission.CAMERA'; // 申请权限
  26. private period: number = 10; // 定义提取视频帧时间间隔
  27. private canIProcess: boolean = false; // 是否需要处理ImageReceiver回图
  28. private cameraResources: CameraResources = {};
  29. /**
  30. * 向用户申请权限
  31. * @returns
  32. */
  33. async requestPermissionsFn(): Promise<void> {
  34. let atManager = abilityAccessCtrl.createAtManager();
  35. if (this.context) {
  36. let res = await atManager.requestPermissionsFromUser(this.context, [this.cameraPermission]);
  37. if (!res || !res.permissions || res.permissions.length === 0) {
  38. hilog.error(DOMAIN, TAG, 'requestPermissionsFromUser interface call fails');
  39. return;
  40. }
  41. if (res.permissions.length !== res.authResults.length) {
  42. hilog.error(DOMAIN, TAG, 'Authentication result mismatch');
  43. return;
  44. }
  45. res.permissions.forEach((value: string, index: number) => {
  46. if (this.cameraPermission.toString() === value && res.authResults[index] !== 0) {
  47. this.context?.getApplicationContext().killAllProcesses();
  48. }
  49. });
  50. }
  51. }
  52. async aboutToAppear(): Promise<void> {
  53. await this.requestPermissionsFn();
  54. hilog.info(DOMAIN, TAG, 'aboutToAppear');
  55. await this.initImageReceiver();
  56. await this.initCamera();
  57. // 设置定时器,定时更新状态位
  58. setInterval(() => {
  59. this.canIProcess = true;
  60. }, this.period * 1000);
  61. }
  62. aboutToDisappear(): void {
  63. hilog.info(DOMAIN, TAG, 'aboutToDisappear');
  64. this.releaseCamera();
  65. }
  66. build() {
  67. Column() {
  68. Text('业务UI界面').font({size: 30, style: FontStyle.Italic})
  69. }
  70. .justifyContent(FlexAlign.Center)
  71. .height('100%')
  72. .width('100%')
  73. }
  74. /**
  75. * 获取ImageReceiver的SurfaceId
  76. * @returns
  77. */
  78. async initImageReceiver(): Promise<void> {
  79. if (!this.imageReceiver) {
  80. // 创建ImageReceiver
  81. let size: image.Size = { width: this.imageWidth, height: this.imageWidth };
  82. try {
  83. this.imageReceiver = image.createImageReceiver(size, image.ImageFormat.JPEG, 8);
  84. } catch (error) {
  85. let err = error as BusinessError;
  86. hilog.error(DOMAIN, TAG, `Init image receiver failed. error code: ${err.code}`);
  87. return;
  88. }
  89. // 获取第一路流SurfaceId
  90. this.imageReceiverSurfaceId = await this.imageReceiver.getReceivingSurfaceId();
  91. hilog.info(DOMAIN, TAG, `initImageReceiver imageReceiverSurfaceId:${this.imageReceiverSurfaceId}`);
  92. // 注册监听处理预览流每帧图像数据
  93. this.onImageArrival(this.imageReceiver);
  94. }
  95. }
  96. // Image格式与PixelMap格式映射关系
  97. private formatToPixelMapFormatMap = new Map<number, image.PixelMapFormat>([
  98. [12, image.PixelMapFormat.RGBA_8888],
  99. [25, image.PixelMapFormat.NV21],
  100. [35, image.PixelMapFormat.YCBCR_P010],
  101. [36, image.PixelMapFormat.YCRCB_P010]
  102. ]);
  103. // PixelMapFormat格式的单个像素点大小映射关系
  104. private pixelMapFormatToSizeMap = new Map<image.PixelMapFormat, number>([
  105. [image.PixelMapFormat.RGBA_8888, 4],
  106. [image.PixelMapFormat.NV21, 1.5],
  107. [image.PixelMapFormat.YCBCR_P010, 3],
  108. [image.PixelMapFormat.YCRCB_P010, 3]
  109. ]);
  110. /**
  111. * 将PixelMap对象的图片数据保存至应用沙箱路径
  112. * @param pixelMap 需要保存的pixelMap对象
  113. * @returns
  114. */
  115. async packToData(pixelMap: PixelMap) {
  116. try {
  117. let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  118. let cacheDir: string = context.cacheDir;
  119. let now = Date.now();
  120. let fileUrl = cacheDir + `/${now}_packToData.jpg`;
  121. if (fs.accessSync(fileUrl)) {
  122. fs.unlinkSync(fileUrl);
  123. }
  124. let packOpts: image.PackingOption = { format: 'image/jpeg', quality: 98 };
  125. const imagePackerApi: image.ImagePacker = image.createImagePacker();
  126. imagePackerApi.packToData(pixelMap, packOpts)
  127. .then((data: ArrayBuffer) => {
  128. let file = fs.openSync(fileUrl, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
  129. fs.write(file.fd, data).then((writeLen: number) => {
  130. hilog.info(DOMAIN, TAG, `packToData succeed and size is: ${writeLen}, path is ${fileUrl}}`);
  131. }).catch((err: BusinessError) => {
  132. hilog.error(DOMAIN, TAG, `packToData failed with error message: ${err.message}, error code: ${err.code}`);
  133. }).finally(() => {
  134. fs.closeSync(file);
  135. });
  136. }).catch((error: BusinessError) => {
  137. hilog.error(DOMAIN, TAG, `packToData error,code ${error.code}, message is ${error.message}`);
  138. });
  139. } catch (err) {
  140. let e: BusinessError = err;
  141. hilog.error(DOMAIN, TAG,
  142. `write file by pixelmap error is: ${JSON.stringify(e.message)} and stack: ${JSON.stringify(e.stack)}`);
  143. }
  144. }
  145. /**
  146. * 注册ImageReceiver图像监听
  147. * @param receiver
  148. * @returns
  149. */
  150. onImageArrival(receiver: image.ImageReceiver): void {
  151. // 注册imageArrival监听
  152. receiver.on('imageArrival', () => {
  153. // 获取并处理当前图像
  154. receiver.readNextImage((err: BusinessError, nextImage: image.Image) => {
  155. if (err || nextImage === undefined) {
  156. hilog.error(DOMAIN, TAG, 'readNextImage failed');
  157. return;
  158. }
  159. hilog.info(DOMAIN, TAG, `image arrived, can i process: ${this.canIProcess}.`);
  160. // 如果当前状态为无需处理,则在释放资源后直接跳过以减轻能耗负载。
  161. if (!this.canIProcess) {
  162. // 注意跳过处理时必须在这里手动释放资源,否则会因为消费端数据堆积而阻塞bufferQueue队列轮转
  163. nextImage.release();
  164. return;
  165. }
  166. this.canIProcess = false; // 如果当前需要处理,则先将状态位更新为无需处理后再处理当前帧数据,防止处理时间过长导致状态更新不及时
  167. // 解析图像内容
  168. nextImage.getComponent(image.ComponentType.JPEG, async (err: BusinessError, imgComponent: image.Component) => {
  169. if (err || imgComponent === undefined) {
  170. hilog.error(DOMAIN, TAG, 'getComponent failed');
  171. }
  172. if (imgComponent.byteBuffer) {
  173. let width = nextImage.size.width; // 获取图片的宽
  174. let height = nextImage.size.height; // 获取图片的高
  175. let stride = imgComponent.rowStride; // 获取图片的stride
  176. let imageFormat = nextImage.format; // 获取图片的format
  177. let pixelMapFormat = this.formatToPixelMapFormatMap.get(imageFormat) ?? image.PixelMapFormat.NV21;
  178. let mSize = this.pixelMapFormatToSizeMap.get(pixelMapFormat) ?? 1.5;
  179. hilog.info(DOMAIN, TAG, `getComponent with width:${width} height:${height} stride:${stride}`);
  180. // pixelMap创建时使用的size、srcPixelFormat需要与相机预览输出流previewProfile中的size、format保持一致,此处format以NV21格式为例
  181. let pixelMap: image.PixelMap;
  182. // stride与width一致
  183. if (stride === width) {
  184. pixelMap = await image.createPixelMap(imgComponent.byteBuffer, {
  185. size: { height: height, width: width },
  186. srcPixelFormat: pixelMapFormat,
  187. });
  188. } else {
  189. // stride与width不一致
  190. const dstBufferSize = width * height * mSize; // 以NV21为例(YUV_420_SP格式的图片)YUV_420_SP内存计算公式:长x宽+(长x宽)/2
  191. const dstArr = new Uint8Array(dstBufferSize);
  192. for (let j = 0; j < height * mSize; j++) {
  193. const srcBuf = new Uint8Array(imgComponent.byteBuffer, j * stride, width);
  194. dstArr.set(srcBuf, j * width);
  195. }
  196. pixelMap = await image.createPixelMap(dstArr.buffer, {
  197. size: { height: height, width: width },
  198. srcPixelFormat: pixelMapFormat,
  199. });
  200. }
  201. await this.packToData(pixelMap!); // 保存图片
  202. await pixelMap.release(); // 释放资源
  203. } else {
  204. hilog.error(DOMAIN, TAG, 'byteBuffer is null');
  205. }
  206. // 确保当前buffer没有在使用的情况下,可进行资源释放
  207. // 如果对buffer进行异步操作,需要在异步操作结束后再释放该资源(nextImage.release())
  208. nextImage.release();
  209. hilog.info(DOMAIN, TAG, 'image process done');
  210. });
  211. });
  212. });
  213. }
  214. /**
  215. * 初始化相机流程
  216. * @returns
  217. */
  218. async initCamera(): Promise<void> {
  219. hilog.info(DOMAIN, TAG, `initCamera imageReceiverSurfaceId:${this.imageReceiverSurfaceId}`);
  220. try {
  221. // 获取相机管理器实例
  222. this.cameraManager = camera.getCameraManager(this.context);
  223. if (!this.cameraManager) {
  224. hilog.error(DOMAIN, TAG, 'getCameraManager call failed');
  225. return;
  226. }
  227. // 获取当前设备支持的相机device列表
  228. this.cameras = this.cameraManager.getSupportedCameras();
  229. if (!this.cameras || this.cameras.length === 0) {
  230. hilog.error(DOMAIN, TAG, 'getSupportedCameras call failed');
  231. return;
  232. }
  233. let cameraDevice: camera.CameraDevice | undefined = undefined;
  234. this.cameras.forEach((device: camera.CameraDevice) => {
  235. if (device.cameraPosition === camera.CameraPosition.CAMERA_POSITION_FRONT) {
  236. cameraDevice = device;
  237. return;
  238. }
  239. });
  240. // 未找到前置摄像头
  241. if (cameraDevice === undefined) {
  242. hilog.error(DOMAIN, TAG, 'Back camera device not found!');
  243. return;
  244. }
  245. // 选择一个相机device,创建cameraInput输出对象
  246. this.cameraResources.cameraInput = this.cameraManager.createCameraInput(cameraDevice);
  247. if (!this.cameraResources.cameraInput) {
  248. hilog.error(DOMAIN, TAG, 'createCameraInput call failed');
  249. return;
  250. }
  251. // 打开相机
  252. await this.cameraResources.cameraInput.open();
  253. // 获取相机device支持的profile
  254. let capability: camera.CameraOutputCapability =
  255. this.cameraManager.getSupportedOutputCapability(cameraDevice, camera.SceneMode.NORMAL_PHOTO);
  256. if (!capability) {
  257. hilog.error(DOMAIN, TAG, 'getSupportedOutputCapability call failed');
  258. this.releaseCamera();
  259. return;
  260. }
  261. // 筛选预览Profile。这里筛选分辨率宽高比1:1,且分辨率>=1080的Profile,开发者可根据业务需要自定义筛选逻辑。
  262. let previewProfile = capability.previewProfiles.find((profile: camera.Profile) => {
  263. return profile.size.width === profile.size.height && profile.size.width >= this.imageWidth;
  264. });
  265. // 使用imageReceiverSurfaceId创建预览
  266. this.cameraResources.previewOutput =
  267. this.cameraManager.createPreviewOutput(previewProfile, this.imageReceiverSurfaceId);
  268. if (!this.cameraResources.previewOutput) {
  269. hilog.error(DOMAIN, TAG, 'initCamera createPreviewOutput');
  270. this.releaseCamera();
  271. return;
  272. }
  273. // 创建录像模式相机会话
  274. let session = this.cameraManager.createSession(camera.SceneMode.NORMAL_PHOTO);
  275. if (!session) {
  276. hilog.error(DOMAIN, TAG, 'session is null');
  277. this.releaseCamera();
  278. return;
  279. }
  280. this.cameraResources.session = session as camera.VideoSession;
  281. // 开始配置会话
  282. this.cameraResources.session.beginConfig();
  283. // 添加相机设备输入
  284. this.cameraResources.session.addInput(this.cameraResources.cameraInput);
  285. // 添加ImageReceiver预览流输出
  286. this.cameraResources.session.addOutput(this.cameraResources.previewOutput);
  287. // 提交会话配置
  288. await this.cameraResources.session.commitConfig();
  289. // 开始启动已配置的输入输出流
  290. await this.cameraResources.session.start();
  291. } catch (error) {
  292. hilog.error(DOMAIN, TAG, `initCamera fail: ${JSON.stringify(error)}`);
  293. this.releaseCamera();
  294. }
  295. }
  296. /**
  297. * 释放相机相关资源
  298. * @returns
  299. */
  300. async releaseCamera(): Promise<void> {
  301. hilog.info(DOMAIN, TAG, 'releaseCamera E');
  302. try {
  303. // 停止当前会话
  304. await this.cameraResources.session?.stop();
  305. } catch (error) {
  306. hilog.error(DOMAIN, TAG, `session.stop call failed, error: ${JSON.stringify(error)}`);
  307. }
  308. try {
  309. // 释放相机输入流
  310. await this.cameraResources.cameraInput?.close();
  311. } catch (error) {
  312. hilog.error(DOMAIN, TAG, `camera close fail: ${JSON.stringify(error)}`);
  313. }
  314. try {
  315. // 释放预览输出流
  316. await this.cameraResources.previewOutput?.release();
  317. } catch (error) {
  318. hilog.error(DOMAIN, TAG, `previewOutput release fail: ${JSON.stringify(error)}`);
  319. }
  320. try {
  321. // 释放会话
  322. await this.cameraResources.session?.release();
  323. } catch (error) {
  324. hilog.error(DOMAIN, TAG, `session release fail: ${JSON.stringify(error)}`);
  325. }
  326. }
  327. }

常见FAQ

Q:代码中canIProcess参数的作用是什么?是否必须要有?

A:on('imageArrival')回调中涉及到PixelMap的创建和处理,这些操作耗费资源比较严重,如果持续处理会对性能造成不小的负担,因此使用canIProcess配合定时器的方式,隔一段时间处理一帧,这样处理既可以节约性能,又与业务场景相契合。考虑到考试场景可能耗时较久,建议还是参考这个处理思路,但如果业务处理逻辑不需要做繁杂的PixelMap处理,也可以考虑移除这套控制逻辑。

在 FAQ 中进行搜索
请输入您想要搜索的关键词