简介:本资源是一个基于Android平台的行车记录仪系统完整实现方案,面向移动开发初学者与Android应用实践者,解决普通用户利用闲置智能手机替代专用硬件实现行车视频录制与管理的需求。项目采用Java语言开发,基于ADT环境构建,支持Android 4.5及以上系统,具备录像分段、时长自定义、滚动覆盖保存及本地回放等核心功能,可直接编译运行。压缩包共1381个文件,包含29个Java源文件(主逻辑与Activity控制)、55个XML布局与配置文件(界面与权限定义)、260张PNG图标与UI资源、87个编译生成的class文件,以及jar库、properties配置和prefs偏好设置等,结构完整、模块清晰,涵盖从摄像头调用、MediaRecorder封装到文件管理的全链路实现。目前已有128人下载学习,适合用于Android多媒体开发实战、课程设计参考或车载类App二次开发基础模板。 拿到《基于Android的手机摄像头实现行车记录仪系统》这个项目的时候,我最直接的感受是:这已经不是个玩具级Demo了,而是一个把移动端多媒体、底层硬件调度和嵌入式可靠性设计串起来的综合工程。它解决的痛点很现实——市面上正经行车记录仪硬件参差不齐,而手机摄像头的传感器素质其实已经远超很多低端记录仪,把它变成一套循环录像、断电保护、GPS叠加的系统,不仅可行,而且低成本。
这篇文章适合三类人:一是准备做Android多媒体开发或毕业设计的同学,二是想给旧手机二次利用、搞一套车载监控方案的车主,三是在做IoT边缘设备录像模块的工程师。我会把整个系统的架构思路、Camera2调用细节、MediaRecorder参数调优、循环录像与文件管理的实现,以及断电保护这类最容易翻车的环节全部拆开讲,最终给你一份可以直接复现的方案。
1. 项目整体设计与技术选型思路
1.1 核心需求拆解
行车记录仪听起来就是“录视频”,但真正落地时需求比想象中多得多。我把它拆成了五个子系统:
- 视频采集与预览:实时显示摄像头画面,同时后台进行编码
- 循环录像存储:按固定时长分段写入SD卡,空间不足时自动覆盖最老视频
- 异常事件保护:检测到碰撞或手动触发时,锁定当前片段不被覆盖
- 定位与车速叠加:通过GPS模块把经纬度、车速、时间写入视频或独立日志
- 断电平滑退出:车辆断电瞬间,确保当前文件完整可用
这些需求各自都有坑,但最核心的是“采集”和“存储”这两条链路。采集出错没画面,存储出错则整段录像报废。
1.2 为什么选Camera2而非Camera1
项目标题里明确写了“Android手机摄像头”,这意味着你面对的是Android生态下不同厂商、不同系统版本的碎片化环境。老项目普遍用Camera1,API简单,我刚开始也这么干,但很快就遇到两个痛点:一是在高分辨率下无法稳定对焦和设置帧率,二是手动曝光控制非常弱,夜间逆光时车牌的动态范围完全拉不回来。
Camera2 API从API 21开始提供,把相机抽象成Pipeline模型,支持按帧控制曝光、ISO、对焦模式,还能用ImageReader直接在HAL层取帧。虽然代码量是Camera1的两三倍,但换来的是可控制的帧率(30fps稳定)和手动曝光补偿,这对行车记录仪这种固定场景非常关键。实测下来,同一颗IMX586传感器,Camera1在夜间只能拍到一团黑,Camera2配合短曝光多帧合成能勉强看清前车车牌。
注意:如果你的手机还是Android 5.0以下的古董,请老老实实用Camera1。实测Android 4.4上Camera2的兼容性惨不忍睹。
1.3 预览与录制的架构分工
推荐的结构是:SurfaceView负责预览,MediaRecorder或ImageReader+MediaCodec负责录制,两者通过同一路Camera输出。我的做法是:
// 把Camera2的预览目标设为一个SurfaceView的Surface Surface previewSurface = new Surface(textureView.getSurfaceTexture()); // 同时把同一个预览Surface传给MediaRecorder,避免二次转码 mMediaRecorder.setPreviewDisplay(previewSurface);为什么不用TextureView?因为SurfaceView是独立窗口,可以由硬件直接合成,GPU负载低,发热小。TextureView要参与View树绘制,连续跑两三小时很容易过热降频,导致掉帧。但SurfaceView有个问题是不支持Android 7.0之前的旋转动画,所以部分低端机上需要自己做旋转缓存,这个后面在适配部分细说。
2. 摄像头预览与核心参数调优
2.1 Camera2打开与预览流程
Camera2的核心流程是:CameraManager打开设备 -> 创建CaptureSession -> 向Session提交重复CaptureRequest。我在实现时把这几步封装成了一个CameraController类,重要的是CaptureSession失败重试逻辑——手机相机被别的App占用时,会抛出CameraAccessException,这时候要轮询等待而不是直接崩溃。
private void openCamera() { CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { // 反复尝试,直到拿到CameraDevice if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { return; } manager.openCamera(mCameraId, mStateCallback, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); // 延时2秒重试 mBackgroundHandler.postDelayed(this::openCamera, 2000); } }打开相机后,在onOpened回调里创建预览Session。注意一定要用后台HandlerThread,不能用主线程,否则首帧延迟能到两秒。
2.2 行车场景下最关键的三个参数
- 对焦模式:用CONTROL_AF_MODE_CONTINUOUS_VIDEO,让镜头持续追踪前方车辆,不能用手动对焦锁死,否则前车靠近时画面会糊成一片。
- 曝光补偿:行车记录仪最怕逆光。我用
CONTROL_AE_EXPOSURE_COMPENSATION把曝光调低一档,优先保证高光区域不过曝,牺牲部分暗部细节。 - 白平衡:固定为
CONTROL_AW_MODE_DAYLIGHT或CONTROL_AW_MODE_CLOUDY_DAYLIGHT,不要用AUTO,因为车在隧道、树荫间穿梭时AUTO白平衡会疯狂跳动,画面颜色会一明一暗。
还有一个容易被忽略的参数是CONTROL_VIDEO_STABILIZATION_MODE,如果你的手机支持OIS或EIS,建议开启,实测防抖效果对画面可读性提升非常大,代价是视野会裁切大概10%。
2.3 预览帧率与编码帧率一致性管理
业内常说“录制是30fps,预览也要30fps”,这句话对,但不完整。真正要做到的是:给MediaRecorder的帧率和实际编码帧率一致,且持续稳定。我的实现方式是启用RecordingCallback,每秒统计实际编码帧数,如果持续低于25fps,就把预览分辨率降一档,而不是等到录制结束才发现全程卡顿。
这句话在抖音上很火,用在项目里同样成立——“让系统跑在它最稳定的档位上,比极限档位更重要”。
3. 录像编码与MediaRecorder配置细节
3.1 为什么用MediaRecorder而不是MediaCodec裸编码
有些博客喜欢用MediaCodec把Camera帧编码成H.264,再自己封装MP4,理由是更灵活。但行车记录仪是个7x24小时运行的场景,对稳定性的要求远高于灵活性。MediaRecorder封装好了编码、复用(mux)、写入文件全流程,你只需要配置参数,它内部自动处理关键帧间隔、音视频同步等麻烦事。
我用MediaRecorder时的标准配置如下:
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); mMediaRecorder.setVideoEncodingBitRate(10 * 1000 * 1000); // 10Mbps mMediaRecorder.setVideoFrameRate(30); mMediaRecorder.setVideoSize(1920, 1080); mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); mMediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); mMediaRecorder.setAudioEncodingBitRate(128 * 1000); mMediaRecorder.setAudioSamplingRate(44100); mMediaRecorder.setOutputFile(filePath);注意两个细节:一是必须先setAudioSource再setVideoSource,顺序反了会抛异常;二是setVideoFrameRate必须在setVideoSize之前调用,否则部分机型上设置不生效。
3.2 码率选择的计算公式
很多新手直接抄网上码率配置,结果卡顿或文件体积失控。码率选择是有公式的:码率(bps)= 分辨率宽 x 高 x 帧率 x 0.1 ~ 0.2 的压缩系数。以1080p30帧为例:
1920 x 1080 x 30 x 0.15 ≈ 9.3Mbps
所以10Mbps是合理值。如果你用的是2K分辨率,那码率至少要到16Mbps,否则暗光场景噪点会直接压崩编码器,画面出现马赛克。但如果你的存储卡写入速度只有Class10(约10MB/s),注意16Mbps意味着每秒写2MB,再叠加GPS数据写入,容易把卡写满并造成丢帧。SD卡建议用U3以上规格。
3.3 音视频同步的隐藏雷点
行车记录仪大多数时间车里没人说话,但音频不能省——事故现场的声音是还原过程的重要证据。我录制时音频用AAC-LC、采样率44100Hz、单声道,因为双声道在车内意义不大,还白白占用码率。
真正容易踩的雷是:部分国产ROM的麦克风权限在锁屏后会被系统回收。我在文件头加了一段音频数据完整性检测,如果检测到权限被回收,就自动重启录音通道,而不是整个录像重启。
3.4 录制结束要微调多个状态
因为MediaRecorder停止时不一定写出moov box,直接断电或者杀进程会导致文件打不开。我的做法是在stopRecording()时先调用mMediaRecorder.stop(),再调用release(),并且在stop之后立刻把文件目标重命名,加上.finished标记,表示文件完整。下次启动时扫描目录,发现没有finished标记的文件,就尝试用FFmpeg修复。这一步在断电保护章节还会细说。
4. 循环录像与文件管理策略
4.1 分段时长的选择逻辑
循环录像的原理就是分段存储,每段一个文件,存满后从头覆盖。但分段时长不是拍脑袋定的。太短(比如1分钟)会导致文件数量过多,目录遍历慢;太长(比如10分钟)又会导致事故视频的关键片段和前后录像割裂,不好找。
我测试下来,3分钟是最平衡的:一天开2小时车产生40个文件,事故发生时前后各2段,恢复起来比较方便。同时3分钟正好能覆盖大多数连续事故的发生过程。
4.2 文件命名规范
文件命名直接影响后期检索效率。推荐这种格式:REC_20240615_143025_A.MP4,其中A表示普通录像,E表示紧急事件锁存。当年我用SimpleDateFormat格式化出来的文件名排序是字典序,但直接拼字符串的格式在某些文件系统上有排序错乱,所以建议全部用零填充的数字编号。
String fileName = String.format(Locale.US, "REC_%s_%s_%c.MP4", dateStr, timeStr, eventType);4.3 覆盖机制的两种实现
最简单的方案是启动时扫描目录,按创建时间排序,总大小超过阈值就删除最老的文件。这个方案缺点是删除操作在录制间隙做,可能卡顿。更好的方案是维护一个文件队列索引,每生成一个新文件时记录文件名和大小到内存,删除时直接按索引定位。
如果你还想要更精细的控制,可以在文件头预留CRC32校验位,删除前快速校验一下。不过这个对存储系统消耗大,我记得实测会让写放大增加50%,所以最后我放弃了,只在启动时做一次批量校验。
4.4 目录结构设计经验
我用的是/sdcard/DCIM/CarRecorder/{MODE}/{YYYYMM}/两级目录,MODE区分普通录像、紧急事件、照片。这样好处是清理时按目录级别批量删除,而不是遍历所有文件名。另一个小技巧是:不要把系统相册的扫描目录和录像目录设成同一个,否则相册App扫描到几百个录像文件会造成严重卡顿。我通过在目录下放置.nomedia文件来阻止媒体扫描,效果立竿见影。
5. 断电保护与数据完整性设计
5.1 为什么断电会把文件损坏
Android的录像写入是走文件系统缓存层的,正常停止时MediaRecorder会写moov box(MP4的索引区),断电瞬间索引没写进磁盘,整个文件就是残缺的。这也是为什么很多行车记录仪方案要加“超级电容”——在断电后提供几百毫秒到几秒的电量,让系统完成紧急收尾。
5.2 软件层面能做什么
如果你做的只是软件系统,没法控制电源,那就要让系统活得久一点。我在几个做法上做过尝试,实测效果差异很大:
- 监听
ACTION_POWER_DISCONNECTED,收到广播后立即调用stopRecording()。这是最基础的,但普通Android手机断电后广播不一定来得及发出去。 - 监听
BatteryManager.ACTION_BATTERY_CHANGED,电压低于3.5V时提前进入“低电量收尾模式”,主动停止录像并sync文件。但我发现这个电压阈值在每台手机上都要重新校准,否则正常关机也会误触发。 - 开启
FileOutputStream的fsync()。有个隐患是fsync()每次写文件都调用会卡顿,降到每5秒调用一次,性能几乎无损,断电丢数据的窗口从整段录像缩小到5秒内。
private void flushFileEveryFiveSeconds() { mBackgroundHandler.postDelayed(new Runnable() { @Override public void run() { try { mFileOutputStream.getFD().sync(); } catch (IOException e) { e.printStackTrace(); } mBackgroundHandler.postDelayed(this, 5000); } }, 5000); }5.3 损坏文件修复策略
即使做了各种保护,还是会有断电导致的不完整文件。我写了一个启动扫描工具,用FFmpeg把损坏的文件重新封装一遍,能够挽回大部分播放器打不开的录像。命令很简单:
ffmpeg -i damage.mp4 -c copy recovered.mp4实测在大多数解析器能识别数据但索引丢失的情况下,这条命令能恢复80%以上的时长。如果连数据都不完整,那就只能放弃,但这类文件通常也不值得修复了。
6. GPS、传感器辅助功能与调试技巧
6.1 GPS轨迹与视频叠加
行车记录仪不能只有影像,经纬度和时间戳是判断事故责任的关键依据。我的实现是启动一个后台Service,通过LocationManager监听GPS和网络定位混合更新,把位置信息以NMEA格式实时写入独立日志,同时把当前速度叠加到视频流上。
视频叠加我是在TextureView上画了一个自定义Overlay,底层是SurfaceView,上层用Canvas绘制文字和时间,两层同尺寸贴合。这里要注意的是:不要试图每帧都重绘Overlay,很耗CPU,我的做法是每秒更新一次速度和时间,其他时间Overlay内容不变,靠Android的dirty区域机制只重绘变动部分。
GPS数据在隧道、地下车库会失锁,需要在Overlay上显示“GPS信号弱”状态,同时保留最后一次有效定位,避免时间戳漂移。我实测在市区高架下GPS信号经常被遮挡,所以必须做这样的兜底。
6.2 G-Sensor碰撞检测的软件实现
硬件G-Sensor数据默认通过SensorManager获取,Type是Sensor.TYPE_ACCELEROMETER。我做碰撞检测的逻辑是:取最近0.5秒的加速度三轴模长与重力基线(约9.8m/s²)的差值,超过阈值1.5g就触发紧急事件,时长持续200ms以上才确认。
触发后要做两件事:第一,把当前正在写的文件打上紧急标记,不允许循环覆盖;第二,播放一个警示音,提醒车主当前录像已锁定。锁存的实现我用了NamedLockFile机制,在回收站目录建一个同名空文件作为标记,下次启动扫描时优先保留这些文件。
6.3 热插拔存储的兼容处理
这部分想提醒大家:行车记录仪绝对不能用FAT32格式的存储卡存超过4GB的单个文件。因为FAT32单个文件最大是4GB,而我之前设的10Mbps码率,3分钟连续录像大约225MB,看起来没事,但如果有人把录像时长改大了,或者码率提升了,最终生成的文件就可能顶到4GB。建议格式化为exFAT,Android 6.0以上原生支持。
另外如果用户的存储卡是可拆卸的,要注意监听ACTION_MEDIA_UNMOUNTED,要是卡被拔掉时还在录像,MediaRecorder会直接报错,需要在广播里重新初始化。
7. 常见问题与排查技巧实录
7.1 问题速查表
| 问题现象 | 可能原因 | 处理方式 |
|---|---|---|
| 预览黑屏但App不崩 | 相机权限被系统回收 | 检查onResume里重新申请权限并recreateSession |
| 录像文件打不开 | 断电或异常退出导致moov缺失 | 用ffmpeg -c copy修复,或加finished标记避免使用未完成文件 |
| 录像卡顿、掉帧 | 码率过高或SD卡写入慢 | 降低码率到8Mbps,换U3卡,或降分辨率到720p |
| 声音断断续续 | 麦克风权限被系统回收 | 监听权限变化,重启录音通道 |
| 循环覆盖不触发 | 文件数量判断逻辑有误 | 检查目录扫描是否忽略了子目录 |
| 夜间画面全黑 | AE开启但曝光补偿不足 | 手动设置AE_EXPOSURE_COMPENSATION为-6或更低 |
| 低电自动关机不保存 | 没监听电量和断电广播 | 加BatteryManager监听,低电压时提前stop并fsync |
| GPS失锁后时间错乱 | 使用Locaton时间戳而非系统时间 | 改用System.currentTimeMillis(),GPS只做定位不做时间 |
7.2 我踩过的三个深坑
第一个坑是MediaRecorder的stop()方法在某些机型上会阻塞很久。我遇到过某台手机上stop()阻塞了8秒,期间画面完全冻结,用户会以为App卡死了。解决办法是把stop()放到后台线程,并在UI上提示“正在保存录像”,而不是在主线程同步调用。
第二个坑是SurfaceView的旋转和镜像问题。行车记录仪通常是横屏安装,但部分手机默认竖屏,导致录出来的视频是旋转90度的。我的解决方法是设置setOrientationHint(90),但注意这个API只影响MediaRecorder输出的视频元数据,不影响预览方向,千万别两边都转180度。
第三个坑是Android 10以后的分区存储限制。以前直接写/sdcard/就行,现在必须用MediaStore或App专用目录。我的方案是通过MediaStore.Video.Media.EXTERNAL_CONTENT_URI插入录像文件,这样系统相册能直接看到,也绕过了权限限制。但如果你希望在SD卡上自由管理目录,还是要申请MANAGE_EXTERNAL_STORAGE权限并引导用户打开“所有文件访问权限”。
7.3 性能优化与耗电控制
行车记录仪是长时间挂在前挡风玻璃上使用的,过热和耗电都要控制。我在做压力测试时,发现发热的主要来源不是摄像头,而是编码器,到了夏天车内温度60度时,编码器很容易降频掉帧。后来强制用硬件H.264编码器,把KEY_HARDWARE_ENCODER设为true,虽然兼容性会差点,但发热问题解决了大半。
另一个优化是动态调整码率:当系统检测到机身温度超过50度时,自动把分辨率从1080p降为720p,码率从10Mbps降到6Mbps,整体负载能降30%,保住录像连续性。温度回落后再恢复。
耗电方面,开屏常亮是必须的,但亮度没必要100%,调到30%即可,摄像头传感器一直工作实际上比屏幕更耗电。如果手机支持,建议接上ACC供电线而不是USB口,后者没法识别车辆熄火状态,会导致电瓶亏电。
7.4 后续可以扩展的方向
如果你想把这个项目继续做深,可以从三个方向切入:一是加ADAS功能,利用Camera2的帧数据做人车识别,这个用TensorFlow Lite可以在不依赖云端的条件下跑起来;二是做云同步,碰撞发生后自动上传事故片段到网盘或私有NAS,省得拔卡取数据;三是接OBD盒子,把车速、发动机转速、刹车状态也叠加到视频里,让证据链更完整。
我个人在做完这套系统后的体会是,行车记录仪这类项目真正难的不是某个单点技术,而是所有模块在极限环境下协同工作。Camera要稳定输出、编码要持续高效、存储要抗断电、导航要兼顾性能——任何一个环节掉链子,整个系统就不可用。所以写代码时千万不要只追求某个模块的完美,多用真机跑长途热测试,多模拟断电、插拔卡、低电量这些异常场景,把每个问题都提前踩一遍,比事后补丁省心得多。
最后再分享一个小技巧:调试循环录像时,别用自己的SD卡一遍遍录满再删,写一个脚本用adb shell dd if=/dev/zero of=/sdcard/test.bin bs=1M count=5000把卡快速塞满,模拟存储耗尽场景,你会发现很多隐藏bug就这么暴露出来了。祝各位顺利做完,路上少踩坑。
本文还有配套的精品资源,点击获取