☰
Delphi 10.2直接驱动Android摄像头:接管预览帧与JPEG回调
2026/10/20 4:20:36 网站建设 项目流程

简介:面向 Delphi 10.2 开发者的 Android 摄像头调用示例工程,基于 FireMonkey 框架演示安卓设备上的常见相机功能,涵盖权限声明、实时预览、拍照录像、前后摄像头切换、对焦变焦及图片数据转换等场景,适合希望快速集成相机功能的应用项目参考。压缩包共 92 个文件,除了 pas 源码、fmx 界面、AndroidManifest 模板、dproj 工程配置外,还包含可直接安装的 apk、9 张截图、5 个 so 动态库以及历史修改版本,整体约 45.78 MB,目录按类型归档,方便按需提取。已有 1206 人学习下载,内容对刚接触 Delphi 安卓原生组件开发的中级程序员比较友好。通过可运行工程,将 TAndroidCamera 组件的属性、事件和控制方法落实到实际代码中,并附上排错思路与部署配置,能帮助理解相机权限、预览绑定、图像格式转换等关键环节,大幅节省环境搭建和问题排查时间。

1. Delphi 10.2 直接驱动 Android 摄像头:不靠 Intent,自己接管预览帧

在 Delphi 10.2 的 Android 工程里直接调摄像头,大多数人第一步就选错了——第一反应是拖一个 TakePhotoFromCameraAction 进去,但那只是把请求丢给系统相机 App,你自己拿不到预览帧,更做不了实时帧分析。这个小项目完全走另一条路:用 Androidapi.JNI.Hardware 的封装直接操作 Camera 硬件,预览、对焦、拍照回调、JPEG 落盘全部自己接管,连取景框都用 SurfaceView 自己画。适合两类人:一类是只想「拍照并返回路径、但不被系统相机界面绑架」的 App 业务;另一类是要做扫码、OCR 框选、实时特征检测、但不打算用原生 Java 重写界面的 Delphi 开发者。按下文把工程跑一遍,半小时内能看到摄像头预览画面出现在你自己画的容器里。

2. 项目定位与技术选型:为什么绕开系统相机的 Intent

2.1 这个 Demo 解决什么问题

很多 Delphi 开发者做 Android 拍照功能时,被官方文档里的 TakePhotoFromCameraAction 带偏了。那个 Action 本质是一次跨进程 Intent 调用,系统相机拍照后把图片 Uri 返回给你。好处是代码少,坏处是:你拿不到预览帧、控制不了闪光灯和对焦策略、界面风格不可控,在部分 ROM 上还会出现拍照后返回慢、图片被压缩的问题。

这个项目封装的是一套直接面向 Camera 服务的调用链。它先把 JNI 层的 Camera 对象拿到手,再通过 SurfaceHolder 把预览画面渲染到你自己指定的 View 上,最后通过 PictureCallback 拿回原始 JPEG 字节流。也就是说,从「打开硬件」到「拿到照片字节」中间的每一帧都经过你的代码,没有被系统 App 插一脚。对于做扫码、OCR、车道线识别这类需要逐帧分析的业务,这才是正确姿势。

2.2 用到的 Delphi 单元与关键组件

工程核心依赖 4 个 Delphi 系统单元,都在 10.2 自带的 SDK 封装里:

单元作用
Androidapi.JNI.Hardware提供 JCamera、JCamera_Params、JCamera_PictureCallback 等相机硬件接口
Androidapi.JNI.GraphicsContentViewText提供 JSurfaceView、JSurfaceHolder 等界面组件接口
Androidapi.JNI.Os / Androidapi.JNI.App提供动态权限申请所需的 TJManifest、Activity 接口
Androidapi.Helpers托管 Activity、Form 与 Native View 之间的转换,以及 TPermissionRequestResult 消息

另外自己写了一个 uCameraHelper.pas,把打开、设参、拍照、释放四个动作收敛成一个类。这样主窗体只负责 UI 和事件响应,不直接碰 JNI 细节。改造时如果想换成 Camera2 API,只需要替换这个类,主窗体逻辑基本不动。

2.3 整体调用链路

摄像头功能从点击按钮到拿到 JPEG,大致走这样一条链路:

  1. 用户点击「打开预览」,先走运行时权限申请;
  2. 权限通过后,Camera.open(0) 拿到后置摄像头句柄;
  3. SurfaceHolder.addCallback 触发 surfaceCreated,此时把 Camera 与 SurfaceHolder 关联并 startPreview;
  4. 点击「拍照」,takePicture 传入三个 callback,其中 JPEG 回调触发后拿到字节数组;
  5. 字节数组写入应用沙盒目录,同时 updatePreview 恢复预览;
  6. 离开页面时在 surfaceDestroyed 或 Form 的 onPause 里 release 摄像头。

其中第 3 步是最容易翻车的地方——很多人直接把 setPreviewDisplay 写在按钮点击里,但那时 Surface 还没创建好,结果是黑屏。后面避坑章节会专门说。

3. 工程搭建与权限配置:Android 6 动态权限是第一道坎

3.1 从空工程到摄像头权限清单

先按常规方式建一个 Delphi 10.2 的 Android 空工程,然后在 Project Options 的 Permissions 页签里勾选 Camera,这一步会同步修改 AndroidManifest.template.xml。如果你用的是自行管理的 manifest,也可以手动加:

<uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera" android:required="true" />

uses-feature 表示该应用强依赖摄像头硬件,Google Play 会据此过滤无摄像头设备。注意:在 10.2 里,如果你只勾选了界面上的 Permission 而没有检查 AndroidManifest.template.xml 里是否真的注入了 CAMERA 权限,就可能在旧设备上遇到 SecurityException。我习惯勾选后打开 manifest 文件确认一遍,避免玄学问题。

3.2 运行时权限申请与结果处理

Android 6 及以上,只写 manifest 还不够,必须在代码里动态申请权限。10.2 没有像高版本那样的 PermissionsService 快捷封装,但可以直接操作 Activity:

procedure TMainForm.RequestCameraPermission; var LPermissions: TJavaObjectArray<JString>; begin LPermissions := TJavaObjectArray<JString>.Create(1); LPermissions.Items[0] := TJManifest.JavaClass.permission.CAMERA; TAndroidHelper.Activity.requestPermissions(LPermissions, 2001); end;

这段代码把 CAMERA 权限字符串包进 Java String 数组,以 2001 作为请求码发给系统。系统弹窗后结果会异步返回,需要在主 Activity 的 onRequestPermissionsResult 通知里接住。10.2 里可通过 TMessageManager 订阅 TPermissionRequestResult 消息:

TMessageManager.DefaultManager.SubscribeToMessage( TMessage<TPermissionRequestResult>, procedure(const Sender: TObject; const M: TMessage) var LPerm: TPermissionRequestResult; begin if M is TMessage<TPermissionRequestResult> then begin LPerm := TMessage<TPermissionRequestResult>(M).Value; if LPerm.RequestCode = 2001 then begin if LPerm.GrantResults[0] = TJPackageManager.JavaClass.PERMISSION_GRANTED then OpenCameraAndStartPreview else ShowMessage('权限被拒绝,无法打开摄像头'); end; end; end);

这里有个容易忽略的点:GrantResults 是 Java int 数组,比较时要和 TJPackageManager.PERMISSION_GRANTED 常量比,不能自己想当然用 0。另外消息订阅最好在 FormCreate 里做,尽量不要在一个局部过程里 subscribe 后马上调用,否则回调还没注册成功。

3.3 预览容器:在 FMX 界面上挂一个 SurfaceView

Delphi 的 TLayout 本身不是原生 View,直接把 Camera 的预览输出指向它是不行的。常见做法是用 JSurfaceView 创建原生视图,再通过 addContentView 加到 Activity 的顶层:

FSurfaceView := TJSurfaceView.Create(TAndroidHelper.Activity); FSurfaceHolder := FSurfaceView.getHolder; FSurfaceHolder.addCallback(Self); FSurfaceHolder.setType(SurfaceType_CAMERA); TAndroidHelper.Activity.addContentView( FSurfaceView, TJViewGroup_LayoutParams.JavaClass.init( TJViewGroup_LayoutParams.JavaClass.MATCH_PARENT, TJViewGroup_LayoutParams.JavaClass.MATCH_PARENT));

setType 在 Android 3.0 之后已经不再建议启用,但老设备上不设置 SurfaceType_CAMERA 可能在预览时报异常,写上这一行可以兼容性兜底。addContentView 那两行参数解释:MATCH_PARENT 让 SurfaceView 铺满整个 Activity 根布局,如果你想让预览画在某个 Panel 区域,可以把这里的参数改成宽高像素值,或者把 SurfaceView 包装进 FrameLayout 再控制位置。

提示:addContentView 添加的是原生 View,它永远盖在 FMX 控件之上。如果需求是「上下留按钮、中间留预览区」,建议在中间留白区域对应的 Native 坐标里,用 setX/setY + setLayoutParams 控制 SurfaceView 的位置,而不是直接用 MATCH_PARENT 全屏。

4. 核心模块拆解:从 Camera.open 到 JPEG 落盘的完整链路

4.1 打开摄像头:前置还是后置

Camera.open 的参数是摄像头 ID。0 是后置,1 是前置。部分国产设备上 ID 可能超过 1,所以更稳妥的做法是先拿 Camera.getNumberOfCameras 再遍历查找:

function TCameraHelper.OpenCamera(AUseFront: Boolean): Boolean; var LCount: Integer; LInfo: JCamera_CameraInfo; I: Integer; begin Result := False; LCount := 0; try LCount := JCamera::JCamera._Creator.getNumberOfCameras; except Exit; end; for I := 0 to LCount - 1 do begin LInfo := TJCamera_CameraInfo.JavaClass.init; JCamera::JCamera._Creator.getCameraInfo(I, LInfo); if (AUseFront and (LInfo.facing = CameraInfo_CAMERA_FACING_FRONT)) or ((not AUseFront) and (LInfo.facing = CameraInfo_CAMERA_FACING_BACK)) then begin FCamera := JCamera::JCamera._Creator.open(I); Result := Assigned(FCamera); Break; end; end; end;

CameraInfo_CAMERA_FACING_FRONT 是硬件层定义的方向常量,取前面加 TJ 前缀的类可以拿到可比较的值。打开后可以顺手读一下分辨率支持列表,后面设参数时就不会设到不支持的尺寸。如果 open 返回空,大概率是摄像头被别的进程占用了,这种情况重试两三次仍是空,就应该提示用户。

4.2 设置预览参数:分辨率与对焦模式

拿到 FCamera 后先 getParameters,改完再 setParameters。直接改 Camera 属性是无效的,必须走参数对象:

var LParams: JCamera_Params; begin LParams := FCamera.getParameters; LParams.setPreviewSize(1280, 720); LParams.setPictureSize(1920, 1080); LParams.setFocusMode(StringToJString('continuous-picture')); LParams.setJpegQuality(90); FCamera.setParameters(LParams); end;

setPreviewSize 决定预览帧的宽高,setPictureSize 决定最终照片分辨率,两者独立。不少开发者只设置了拍照分辨率,预览还是默认的 640x480,看起来非常模糊。setJpegQuality 是 JPEG 压缩质量,范围 1 到 100,90 是速度与画质的折衷点。如果做 OCR,建议降到 80,能明显加快回调速度,对识别率影响可以忽略。对焦模式里,'continuous-picture' 是最常用的拍照场景模式;做扫码可以改用 'continuous-video',帧率会更平滑。

4.3 预览回调:把 Camera 输出绑定到 SurfaceHolder

预览能显示的前提是 Surface 已经准备好。所以回调方法必须实现 JSurfaceHolder_Callback 的三个方法:

procedure TMainForm.surfaceCreated(holder: JSurfaceHolder); cdecl; begin if Assigned(FCamera) then begin FCamera.setPreviewDisplay(holder); FCamera.startPreview; end; end; procedure TMainForm.surfaceChanged(holder: JSurfaceHolder; format: Integer; width: Integer; height: Integer); cdecl; begin // 横竖屏切换时 Surface 会重建,这里需要重新启动预览 if Assigned(FCamera) then begin FCamera.stopPreview; FCamera.setPreviewDisplay(holder); FCamera.startPreview; end; end; procedure TMainForm.surfaceDestroyed(holder: JSurfaceHolder); cdecl; begin ReleaseCamera; end;

surfaceCreated 是首个回调,此时 Surface 已经可用。setPreviewDisplay 把 Camera 输出定向到这个 Surface 上,之后 startPreview 才开始出帧。surfaceChanged 在 Surface 尺寸变化或旋转时触发,最稳妥的处理就是停掉预览、重新绑定、再启动。surfaceDestroyed 是和 release 最匹配的位置,在这里释放可以避免 Activity 退出后摄像头仍在工作。

4.4 拍照回调:JPEG 字节从硬件到内存

拍照通过 takePicture 完成,它接收 4 个回调:ShutterCallback、RawCallback、PostViewCallback、JpegPictureCallback。实际使用中后两个通常传 nil,因为大多数场景只需要最终 JPEG:

FCamera.takePicture(nil, nil, nil, Self);

Self 之所以能传进来,是因为主窗体声明时实现了 JCamera_PictureCallback 接口。接口里的方法只有一个:

procedure TMainForm.OnPictureTaken(Data: TJavaArray<Byte>; Camera: JCamera); cdecl; begin if Assigned(Data) and (Data.Length > 0) then SaveJpegToFile(Data) else ShowMessage('拍照失败,未拿到图片数据'); end;

Data 是完整 JPEG 字节流,不是文件路径。这个特性和系统相机 Intent 完全不同:字节流可以让你在落盘前直接做压缩、裁剪、灰度化,无需先从文件读一遍。回调执行在 camera 的 binder 线程上,不要在回调里直接更新 UI,把字节流转到主线程再弹提示或刷新界面。

4.5 保存 JPEG 与释放摄像头

拿到字节后最直接的做法是写到应用沙盒目录,避免申请存储权限:

procedure TMainForm.SaveJpegToFile(const AData: TJavaArray<Byte>); var LPath: string; LStream: TFileStream; begin LPath := TPath.Combine(TPath.GetDocumentsPath, Format('camera_%s.jpg', [FormatDateTime('yyyymmddhhnnsszzz', Now)])); LStream := TFileStream.Create(LPath, fmCreate); try LStream.Write(AData.ToBytes[0], AData.Length); finally LStream.Free; end; end;

写应用私有目录不需要 READ/WRITE_EXTERNAL_STORAGE 权限,这是很多项目的隐藏坑:如果你随手把图片写到公共 DCIM 目录,Android 6+ 就必须动态申请存储权限,否则 FileNotFoundException 或者 SecurityException 随机出现。文件命名的时间戳精确到毫秒,可以防止连续拍照时覆盖。

释放摄像头时要注意顺序:先 stopPreview 再 release,release 后必须把对象置 nil,否则下一次 open 会直接抛 RuntimeException:

procedure TMainForm.ReleaseCamera; begin if Assigned(FCamera) then begin FCamera.stopPreview; FCamera.release; FCamera := nil; end; end;

release 是一个耗时的硬件操作,在 Android 主线程里调用虽然不报错,但会有几十到上百毫秒的卡顿。如果页面切后台比较频繁,可以考虑把 release 放到一个延迟任务里执行,但要先 stopPreview 避免新预览帧继续往一个已释放的 Camera 上送。

5. 避坑记录:黑屏、照片旋转、权限二次弹窗、资源占用

5.1 预览黑屏,Surface 还没好你就 startPreview 了

现象:点击按钮后 Camera 打开了,屏幕上却一片黑,logcat 里经常出现 startPreview 失败或 setPreviewDisplay failed。 原因:SurfaceView 的 Surface 创建是异步的,按钮点击发生在 surfaceCreated 之前,此时 setPreviewDisplay 找不到可用的 Surface,预览自然出不来。 解决:把 setPreviewDisplay 和 startPreview 全部放进 surfaceCreated 回调里,用回调驱动相机启动,而不是用按钮直接启动。

5.2 拍出来的照片自动旋转 90 度

现象:竖屏应用预览是正的,拍完打开相册发现照片横了。 原因:摄像头传感器默认横向放置,Java 层拿到的 JPEG 不带旋转信息,也没有自动补偿。 解决:在拍照前根据 CameraInfo.orientation 计算旋转角,然后 setDisplayOrientation 修正预览方向;JPEG 落盘时用 Exif 接口写入 orientation 标签,或者直接把 JPEG 字节解码后旋转再编码。一般后置摄像头 orientation 是 90,前置是 270,具体要以设备实测为准。

5.3 用户拒绝后第二次申请权限弹窗不出现

现象:第一次点「拒绝」,第二次调 requestPermissions 完全没有反应,功能直接不可用。 原因:系统会把第一次拒绝的请求记住,如果用户勾选了「不再询问」,后续申请直接走拒绝分支,不会再弹窗。 解决:在申请前先判断 shouldShowRequestPermissionRationale,返回 false 且权限未授予时,说明被彻底拒绝,此时引导用户去系统设置页手动打开。设置页可以用 Intent 带 package uri 跳转,这里需要注意申请代码要跑在 UI 线程上。

5.4 退出页面后摄像头被其他应用占用

现象:从页面返回后,再进来自拍,打开前置摄像头报错 Camera open failed。 原因:Form 销毁时没有调用 release,或者 release 被写在一个永远不执行的清理分支里,摄像头硬件一直保持 open 状态。 解决:把 ReleaseCamera 同时挂在 FormDestroy 和 Application 的 onEvent 里处理 APP 切后台的场景。只要摄像头未释放,其他应用打开相机也会失败,用户会以为是自己手机坏了。我的经验是「surfaceDestroyed → 切后台 → 窗体销毁」三个点都调用 release,并保证 release 方法内部做了空指针判断,不重复释放。

6. 二次开发改造思路:把拍照 Demo 改成帧分析引擎

先弄清楚业务需要的是「单张拍照」还是「连续分析」。如果是 OCR 或扫码,重点不在 takePicture,而在预览帧回调。Camera 支持 setPreviewCallbackWithBuffer,把每一次预览帧的 byte 数据回调出来:

FCamera.setPreviewCallbackWithBuffer(FPreviewCallback); // 配合 addCallbackBuffer 循环复用 byte 数组

然后在回调里做灰度转换、二值化,再把结果交给你的识别算法。注意预览格式要设置成 ImageFormat.NV21,这个格式在 Android 相机上支持最广,转灰度最快,Delphi 里对应的是 TJImageFormat.JavaClass.NV21。

如果目标设备都是 Android 7 以上,建议直接升级 Camera2 API。Camera2 在 10.2 里有 Androidapi.JNI.Hardware.Camera2 封装,核心入口是 TJCameraManager.openCamera,支持手动拼接 Session 和反复提交 CaptureRequest。改造时把 uCameraHelper 的接口保持不变,内部实现从 Camera 换成 Camera2,主窗体的调用代码不需要大改。

这个 Demo 我前后在真机上翻过三次车,最典型的就是黑屏和旋转问题。从那以后我每次做 Android 相机功能都会强制走一遍「权限 → Surface → 预览 → 回调 → 释放」的五步链路,确认每一步都有日志或可见反馈,再做下一步。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询