简介:本资源是一个基于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二次开发基础模板。 做行车记录仪App,其实没有想象中那么玄学。一个Android手机、一个车载支架、再加一个充电头,配合自研的一套录制系统,就能顶替市面上几百块的行车记录仪。这个“基于Android的手机摄像头实现行车记录仪系统”的项目,干的就是这件事:用手机摄像头完成持续录像、循环覆盖、碰撞锁定、GPS数据记录等核心功能。成本几乎为零,代码还能复用,对Android开发者来说,既是一个练手的好项目,也是可以快速落地的车载方案原型。
这个项目适合两类人:一是想自己动手给车做个备用记录仪的开发者,二是准备做车载相关的Android应用、想了解Camera2、MediaRecorder、前台服务这套完整链路的朋友。下面我从需求拆解开始,一步步把整个系统的实现思路和技术细节说清楚,最后附上我在实际调试中踩过的坑和排查方法。
1. 项目本质与需求拆解
1.1 行车记录仪真正要解决的核心问题
很多人觉得行车记录仪就是“一直录像”,其实真要按照这个思路做,做出来的东西根本没法用。我从实际使用的角度拆解一下,一个合格的记录仪系统必须解决四件事。
一是循环录像。车载存储空间是有限的,记录仪必须做到满了以后自动覆盖最早的文件,而不是录满就停。这就要求在存储策略上做“时间片轮转”,每个文件有固定的时长,系统定期检查磁盘剩余空间,按时间顺序清理最老的录像。
二是证据锁定。发生剐蹭或事故的时候,这一段视频不能被循环录像覆盖掉。卡车记录仪的解决方案是碰撞传感器触发“事件录像”,把当前片段和前一分钟、后一分钟的视频复制到独立目录,或者给文件打上标记,清理时跳过。手机端的加速度传感器完全可以实现同样的效果。
三是断电保护。车载记录仪的点烟器供电在熄火后会断电,这时候要干净利落地保存当前录像文件并退出录制,不能留下一个损坏的MP4。Android平台上的做法是监听ACTION_POWER_DISCONNECTED广播,在断电瞬间完成文件收尾。
四是状态可视化。用户需要知道“它正在录”,尤其是手机方案里,用户本来就对稳定性有疑虑。所以界面上要有明确的录制状态、时间、GPS信号强度,最好还能画中画显示,方便随时确认。
这个项目最终落地的方案就是用前台服务+Camera2+MediaRecorder+传感器+位置服务,把这几件事完整串起来。
1.2 技术选型:录制方案和相机API怎么选
先说相机API。Android平台有Camera、Camera2、CameraX三条路可选。Camera老API已经废弃,对于高分辨率视频和动态切换支持很差,直接排除。Camera2是现代Android系统的主推方案,灵活性最高,代价是代码量大、状态机复杂。CameraX是对Camera2的封装,内置生命周期绑定和用例抽象,代码量能减少一半左右,但细节控制能力弱一些。这个项目最终选了Camera2,原因很简单:车载场景要精确控制预览尺寸、录制尺寸、帧率和刷新切换,CameraX在处理MediaRecorder的Surface连接时,偶尔会有底层时序问题,而Camera2在这方面的链路是完全可控的。
再说录制方案。同样有三条路:MediaRecorder、MediaMuxer、MediaCodec+MediaMuxer手动编码。MediaRecorder是一个高封装度的终端类,底层自动完成编码、封装、文件写入,和Camera2的SurfaceMode组合使用,代码量非常小,稳定性也不错。MediaMuxer适合做多路音视频合成、自定义封装,但对实时编码需要自己配合MediaCodec,复杂度高很多。MediaCodec裸方案适合要对视频帧做AI处理或者特殊叠加的场景,项目里如果只是普通录制,属于杀鸡用牛刀。所以方案定为Camera2+MediaRecorder的SurfaceMode,这也是Android官方推荐的极简录制方案。
1.3 业务模块划分
我按照功能把系统拆成五个模块:摄像头采集模块负责打开相机、选择输出尺寸、提供Surface;录制模块负责MediaRecorder的启动、停止、切片和文件落盘;持久化服务模块负责前台服务的创建、保活、断电监听;传感器模块负责加速度碰撞检测和GPS记录;文件管理模块负责循环清理和事件录像的锁定保护。
这样划分的用意很直接:摄像头和录制是两条相对独立的流水线,前面一端崩溃不能影响文件端的完整性;传感器和GPS是旁路功能,即使摄像头出问题,碰撞信息和最后的位置也要保留下来。下面逐个模块说实现细节。
2. 摄像头采集与MediaRecorder录制链路
2.1 打开摄像头:尺寸选择和帧率权衡
摄像头的打开逻辑不复杂,但参数选择我最想重点说。Camera2里,打开摄像头要用CameraManager.openCamera,然后通过CameraDevice.createCaptureSession建立会话。关键是选择录制尺寸:我实测了大量设备后,稳定推荐1280x720分辨率、30帧率。为什么不推荐1080p?因为手机摄像头在车载场景下是长期连续工作,车内温度高,1080p编码会让发热量显著增大。我在夏天车内实测,1080p录制半小时后,iPhone和多数安卓手机都会触发过热保护,而720p能稳定跑完整段路程。而且行车记录仪的主要用途是看清车牌和事故过程,720p在白天完全够用,夜间稍差但也能分辨主要细节。
帧率方面,30fps是录制稳定性和流畅度的平衡点。60fps会让码率翻倍,存储压力大,而且弱光下噪点明显,价值不大。
选择尺寸的代码逻辑是这样的:
CameraManager manager = (CameraManager) getSystemService(CAMERA_SERVICE); Size bestSize = null; MediaCodecInfo.VideoCapabilities caps = manager.getCameraCharacteristics(cameraId) .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP) .getVideoSizes(MediaRecorder.VideoSource.SURFACE); // 从所有支持的尺寸中,优先找1280x720 for (Size size : caps.getSupportedSizes()) { if (size.getWidth() == 1280 && size.getHeight() == 720) { bestSize = size; break; } }这里有个容易踩的坑:getVideoSizes返回的不仅是录制尺寸,还包括各尺寸对应的帧率范围。判断是否支持某个帧率,要用VideoCapabilities.areSizeAndRateSupported(width, height, 30.0)。有些低端设备宣称支持1920x1080,但30帧率实际上不支持,强行录制会出现文件时间轴加速或慢放的问题。
2.2 MediaRecorder核心配置与Surface模式
MediaRecorder配合Camera2录制,最大的优势是Surface模式下的“零拷贝”。Camera2的输出目标可以是一个Surface,MediaRecorder通过createInputSurface创建另一个Surface,把两个Surface加入同一个CaptureSession,视频数据直接从相机传感器流转入编码器,中间不经过CPU。
关键配置如下:
MediaRecorder recorder = new MediaRecorder(); recorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); recorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER); recorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); recorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC); recorder.setVideoSize(1280, 720); recorder.setVideoFrameRate(30); recorder.setVideoEncodingBitRate(4_000_000); recorder.setAudioEncodingBitRate(96_000); recorder.setAudioSamplingRate(44_100); recorder.setOrientationHint(90); recorder.setOutputFile(filePath); Surface inputSurface = recorder.getSurface(); recorder.prepare();参数里有两个最重要的。一个是setOrientationHint(90),手机横屏固定在支架上时,如果不设置旋转角度,录出来的视频方向就是歪的。取向宏是按照手机固定方向计算出来的,横向支架时传感器方向和显示方向相差90度或270度,具体值要根据设备实测调整。另一个是码率,720p视频我按4Mbps给,这个码率下画质清晰且文件体积可控。音频采样率设为44.1kHz,兼容性最好,48kHz在某些老设备上会有音画不同步问题。
2.3 循环录像切片的几种实现方式
行车记录仪不能一个文件录到天荒地老,必须切成几分钟的片段。切片有三种实现,我分别说优缺点。
第一种是MediaRecorder自带的setMaxDuration(180000),到时间自动停止录制,然后监听onInfo回调再启动下一个文件。这个方案简单,但两个文件之间会有两三秒的切换空档,关键瞬间容易被漏掉。
第二种是Android 8.0之后MediaRecorder提供的setNextOutputFile,可以让录制流程无缝衔接。做法是在当前文件录制前,提前把下一个文件的file descriptor传进去,当前文件到时自动关闭,新的文件自动开始。实测切换延迟可以控制在几十毫秒内,核心证据基本不会被漏掉,是目前最优方案。
第三种是MediaMuxer手动封装,自由度最高,但编码和封装都要自己做,稳定性需要大量适配。
我最终使用的是第二种方案,配合一个“双文件预创建”机制:录制启动时同时为当前文件和下一个文件分配路径,然后调用setNextOutputFile,这样切文件的时候不会卡在文件创建上。
循环清理逻辑放在统一的FileManager里,每个文件按命名规则带上时间戳:REC_20240101_093000.mp4。清理线程每隔一分钟扫描一次存储目录,按时间戳升序删除最早的文件,直到剩余空间超过设定的阈值。阈值我设为500MB,这个值确保在临界状态下还能继续录制至少15分钟。
2.4 碰撞事件录像的保护机制
碰撞检测触发后,要做的不只是弹个提示,核心是保护录像文件不被循环清理。我采用的做法是给文件加后缀标记。正常录像文件命名为REC_时间戳.mp4,碰撞锁定的文件改名为EVENT_时间戳.mp4,清理逻辑中只删除REC开头的文件,EVENT文件默认保留90天再清理。
这里有个细节:如果在切片过程中刚好发生碰撞,当前正在写的文件不能直接改名,否则MediaRecorder的句柄会出问题。正确做法是记录下来“文件路径+碰撞时间”,等该文件录制完整停止后,再通过renameTo改名。碰撞发生后,我会同时把碰撞前一个文件和碰撞后一个文件都锁定,这样事故前后的完整证据链都有。
2.5 多摄像头支持:前后切换
行车记录仪还需要支持前摄和后摄切换,例如一些人习惯后摄记录下来到车过程中的后方情况。Camera2的CameraManager.getCameraIdList可以列出所有摄像头,通过LENS_FACING_FRONT和LENS_FACING_BACK区分方向。切换时先停止当前录制会话,释放CameraDevice,再重新打开新的摄像头并重启MediaRecorder。我实测切换耗时一般在1.5秒到2秒之间,中间会有一段空白,但这对行车记录仪场景可以接受。
3. 后台常驻与断电保护:车载场景的生命周期
3.1 前台服务与Android 14的serviceType要求
行车记录仪App一旦进入后台,系统随时可能回收进程,所以必须用前台服务保活。前台服务通过通知栏常驻通知让用户明确知道录制状态。这个项目里,前台服务必须在onStartCommand中启动录制,并返回START_STICKY,这样即使系统杀掉了服务,也会在稍后重新创建。
这里要特别提醒Android 14(targetSdk 34+)的改动:前台服务必须声明foregroundServiceType,摄像头和麦克风的类型是camera和microphone。Manifest里要这样写:
<service android:name=".RecordingService" android:foregroundServiceType="camera|microphone" android:exported="false" />同时运行时还需要权限:
if (Build.VERSION.SDK_INT >= 30) { ServiceCompat.startForeground(service, NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA | ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE); } else { service.startForeground(NOTIFICATION_ID, notification); }有个坑是,Android 14要求启动camera或microphone类型前台服务时,必须先获得对应的运行时权限,否则会抛SecurityException。我在项目里把摄像头、麦克风、定位这三个权限做成启动前的强制检查,缺一个就在引导页提示。
3.2 熄屏录制与WakeLock, 以及后台摄像头限制
行车记录仪在车辆行驶中通常是熄屏状态,但Android系统进入休眠后会暂停CPU任务,录制自然就断了。解决方案是获取PARTIAL_WAKE_LOCK:
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE); wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DashCam:Recording"); wakeLock.acquire();PARTIAL_WAKE_LOCK允许CPU持续运行,但不点亮屏幕,功耗控制比较理想。实测一个多小时录制,CPU耗电在15%到20%左右,配合车载充电完全无压力。
还有一个隐藏很深的坑:Android 9.0开始,系统限制后台应用访问摄像头和麦克风。如果应用没有可见的Activity在前台,打开摄像头会直接失败。行车记录仪App在前台启动录制后,用户切到后台,几分钟后会触发这个限制。
解决方法是画中画(PiP)。PiP模式下,系统认为应用仍然在前台有视觉组件,可以继续访问摄像头。实现方式是:录制服务启动后,把当前Activity切换到PiP模式,界面上显示一个小窗口,里面是预览画面。这样既满足了系统限制,又给用户提供了监控画面的作用。
PictureInPictureParams params = new PictureInPictureParams.Builder() .setAspectRatio(new Rational(16, 9)) .build(); enterPictureInPictureMode(params);Android 12之后的版本还需要在Manifest里声明android:supportsPictureInPicture="true",否则PiP按钮和接口会失效。
3.3 断电监听:干净地收尾
断电保护是这个项目里最考验细节的地方。车载USB口在熄火断电的瞬间,系统会先收到ACTION_POWER_DISCONNECTED广播,然后过几秒才真正关机。这个时间窗口足够做文件收尾。
我在Manifest里动态注册广播接收器:
IntentFilter filter = new IntentFilter(); filter.addAction(Intent.ACTION_POWER_DISCONNECTED); filter.addAction(Intent.ACTION_SHUTDOWN); registerReceiver(powerReceiver, filter);收到断电广播后,立刻做两件事:一是把当前录制文件通过MediaRecorder.stop()落盘,二是停止所有后台线程并置位“已断电”状态,防止系统关机后又有残余线程写文件导致损坏。实测中,从断电广播到文件完全保存需要约1到2秒,而手机断电后还有短暂延迟才真正关机,这个窗口是够的。
需要注意的是,很多车的点烟器口熄火后仍有短暂供电(俗称“延时断电”,从几秒到几分钟不等)。广播方式只处理系统层面的断电,车辆电源断开和手机检测到断电之间还有差别。更可靠的保障是硬件方案:使用带超级电容的车载充电器,它能在断电后为手机多续几秒的电。软件层面能做的是尽量缩短切片文件时长,事故时最后一段录制的最大损失时间不会超过切片时长。我最终把切片定为1分钟,兼顾文件大小和断电窗口。
3.4 前台上部显示:确保用户可见
行车记录仪的监控界面应该是一个常驻的、信息丰富的界面。我的界面包含:录制时间、事件状态、GPS信号、速度、录像分辨率、存储剩余空间。顶部用小圆点+秒表显示正在录制的状态,这个设计借鉴了运动相机的界面逻辑,用户一眼能看出来系统是否在工作。
4. GPS、加速度传感器与附加能力
4.1 GPS数据记录与回放
行车记录仪的证据价值很大程度来自位置和速度信息。我在服务里启动LocationManager的requestLocationUpdates,每2秒请求一次GPS信息。位置信息两条线并行使用:一条是叠加到视频画面上的速度/坐标字符串,另一条是独立生成一个NMEA格式的log文件,和视频保存在同一目录下。这样事后看视频时,可以通过时间戳把位置信息同步到视频上,即使画面没有叠加信息,也有后处理的空间。
叠加坐标的简单实现是通过TextView直接画在预览界面上,再用MediaRecorder.setCustomInfo写入视频元数据。不过setCustomInfo是API 30新增的接口,兼容性有限。我的做法是给预览Surface套一个自定义OverlayView,直接绘制文本到画面上,录制出来的视频自带数据。
4.2 加速度碰撞检测与灵敏度调参
碰撞检测依赖SensorManager.TYPE_ACCELEROMETER。加速度传感器返回X、Y、Z三个轴的数据,单位是m/s²(重力加速度约9.8 m/s²)。算法逻辑非常直观:计算三轴加速度的合成模值,减去重力加速度后判断是否超过阈值。
@Override public void onSensorChanged(SensorEvent event) { float x = event.values[0]; float y = event.values[1]; float z = event.values[2]; double magnitude = Math.sqrt(x * x + y * y + z * z); double delta = magnitude - SensorManager.GRAVITY_EARTH; if (Math.abs(delta) > 2.5 * SensorManager.GRAVITY_EARTH) { triggerEventLock(); } }阈值设多少合适?我试过1.5倍、2.0倍、2.5倍。2.5倍(约24.5 m/s²)起步,这样过减速带、压井盖这些轻微振动不会误触发,真正追尾和剐蹭都能捕获。如果你停车环境经常被旁边的车开车门碰到,可以调到2.0倍提高灵敏度,但误报也会相应增加。
这里还有一个小细节:传感器事件在主线程回调时会阻塞UI,需要在SensorManager.registerListener时指定SENSOR_DELAY_UI以上的频率即可,不要用SENSOR_DELAY_FASTEST,否则耗电会上升。实际项目中我设置在SENSOR_DELAY_GAME,大约20ms一次回调,对碰撞检测完全足够。
4.3 存储容量估算与循环清理算法
很多人忽略存储容量预算,直接用默认设置,结果录一天就满了。我按4Mbps码率算:视频每秒约500KB,加上音频约12KB/s,总计约512KB/s。一分钟约30MB,一小时约1.8GB。假设手机剩余可用空间为25GB,能连续录制约13小时。如果每天通勤2小时,循环覆盖完全够用。
清理算法需要注意一个边界条件:正在录制的当前文件不能删。否则MediaRecorder会在文件被删除后继续写入而找不到路径,导致录制失败。我的保护逻辑是,记录当前正在写入的文件名,清理时排除它,同时排除EVENT文件。
这里我还遇到一个文件系统的问题:Android的FAT32格式文件系统单文件上限是4GB,而MediaRecorder在接近4GB边界时可能有未知行为。我的切片策略是1分钟一个文件,单个文件约30MB,完全规避了这个风险。
5. 常见问题排查与调试实录
5.1 MP4文件损坏:3秒黑文件问题
这个问题的表现是:录像文件存在,但是播放器打不开,或者只能看到前3秒画面。我在项目初期经常遇到,排查了很久,最终定位到两个原因。
第一个原因是录制过程中Activity或服务被系统回收,MediaRecorder没有正常执行stop(),文件没有写入moov box(MP4的索引信息)。解决方法是加一个最终的写入兜底:在MediaRecorder.start()之后的任何异常路径,都try-finally执行stop()。
第二个原因和setMaxDuration有关。如果录制时间到达上限后自动停止,但新文件还没准备好,旧文件的收尾就会被跳过。这个场景正是我前面说到的为了切片稳定改用setNextOutputFile的原因。
最后还有一个很隐蔽的点:不要在onPause中立即调用stop()。由于Activity的onStop和onPause在系统断电时可能晚于或早于文件落盘时机,我用一个RecorderStatus枚举状态机来管理,只有状态处于“RECORDING”时才允许执行stop,其他状态一律忽略,避免重复stop导致的崩溃。
5.2 后台录制等机型差异问题
手机方案最头疼的就是厂商ROM的差异。常见问题包括:小米手机锁屏后应用被清理,华为P/Mate系列白名单机制,一加的电池优化默认设置为“智能限制”,OPPO/vivo的“自启动”权限未开启等。这些系统在默认状态下会严重影响后台录制。
实际项目中,我做了三件防御性工作。第一,在首次启动的引导页明确提示用户去系统设置里关闭电池优化、开启自启动、锁定后台任务。第二,在代码里通过PowerManager.isIgnoringBatteryOptimizations检查,并引导用户跳到设置页。第三,即使进程被杀死,在前台服务使用START_STICKY拉起后,再通过ActivityManager.getRunningAppProcesses判断当前是否存活,如果被杀则从最近一个完整的文件继续录制,而不是从头开始。
5.3 文件导出与Android分区存储
将录像导出到电脑或者传给交警,需要保证文件可被外部访问。Android 10以后,WRITE_EXTERNAL_STORAGE权限已经失效,不能直接往公共存储目录随便写文件。我的实现方案是把录像保存在应用专属外部目录getExternalFilesDir(Environment.DIRECTORY_MOVIES),再用FileProvider共享:
<provider android:name="androidx.core.content.FileProvider" android:authorities="${applicationId}.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" /> </provider>这样在录像回放界面点导出时,通过FileProvider.getUriForFile生成content:// URI,配合Intent.FLAG_GRANT_READ_URI_PERMISSION分享到微信、邮件或网盘。很多人遇到“文件能看但分享不了”的问题,基本都是忘记加grantUriPermissions导致的。
5.4 画面花屏、噪声和摄像头失效排查
还有一类问题在车载场景更容易出现:高温下摄像头模组或摄像头驱动异常,导致画面花屏、偏色、黑屏。这类问题常见原因有三个:一是摄像头镜头脏污,尤其前挡风玻璃处灰尘多,现象是画面整体模糊或局部污点,清洁即可;二是手机温度过高触发了图像质量下降或摄像头被系统关闭保护,这个无法绕过,只能通过降低录制分辨率、关掉屏幕亮度来缓解;三是硬件老化导致的对焦马达卡死,表现为画面长时间无法合焦。软件层面能做的排查是看Camera2的CaptureCallback返回的错误码,如果是ERROR_CAMERA_IN_USE或ERROR_CAMERA_DISABLED,说明摄像头被其它应用占用或系统策略禁用。
6. 写在最后
这个项目前后迭代了差不多三周,从第一版“能录像”到最终“能长期稳定运行”,中间大量时间不是花在功能实现上,而是花在适配和保活上。做这类车载Android项目,我的体会是:功能层面的代码量其实不大,真正决定成败的,是对系统资源管理、生命周期边界和异常路径的处理。
如果后续想继续扩展,有几个方向值得一试:把录像上传到NAS或云存储,实现停车后的远程查看;接入三方OBD盒子,把车速、转速真正从CAN总线上取下来叠加到画面;或者把传感器数据做成更完整的事故分析报告,碰撞时自动保存前后各30秒的关键片段。这套基于手机摄像头的方案虽然跟专业硬件记录仪比还有差距,但胜在灵活、可定制、成本可控,作为个人项目和车载应用原型,已经足够实用了。
最后再分享一个小技巧:如果你打算上车实际测试,一定准备一个带电量显示的车充,一边录制一边观察温度和电流变化,这个过程中暴露出来的续航和散热问题,是任何模拟器都测不出来的。
本文还有配套的精品资源,点击获取