在Android平台下,实现车载导航的需求。本文主要介绍的是在Android平台下对行车记录仪相关功能的实现和设计。
1. 实现功能
目前可以实现如下功能
(1) 前后摄像头同时预览;
(2)usb摄像头和后置摄像头同时预览;
(3)单独录制前摄像头和后置摄像头;
(4)同时录制usb 摄像头和后置摄像头。
2. 应用使用方法
(1)首次进入应用,需要勾选确认一些操作权限;
(2)默认的显示页面是USB摄像头和后置摄像头的预览界面;
(3)点击录像按钮,开始录像,显示已录像时长;再次点击按钮,停止录像;
(4)录像的视频文件保存在Picture/CarRecorder/下面。
3. 分屏架构设计
由于考虑到界面的分屏显示,需要用到两个操作界面,并且两个界面互不冲突。有两种方案可供选择:
(1)利用Android7.0分屏新特性
(2)使用fragment进行嵌套
对于第一种方案,由于Android7.0的分屏新特性不支持同一应用内的分屏操作,故放弃此方案。CarRecorder应用采用的是第二种方案,利用多个fragment嵌套在同一个activity中,实现预览界面的分屏。
Fragment 表示 Activity 中的行为或用户界面部分。可以将多个片段组合在一个 Activity 中来构建多窗格 UI,以及在多个 Activity 中重复使用某个片段。可以将片段视为 Activity 的模块化组成部分,它具有自己的生命周期,能接收自己的输入事件,并且可以在 Activity 运行时添加或移除片段(有点像可以在不同 Activity 中重复使用的“子 Activity”)。
Fragment 必须始终嵌入在 Activity 中,其生命周期直接受宿主 Activity 生命周期的影响。在CarRecorder应用中,MainAcitivty是所有fragment的宿主activity,其控制着fragment的生命周期。
下图是activity与fragment相对应的生命周期总结:
对应的效果图如下:
4. 目录结构
(1)java类文件
在项目中的src目录中:
BackCameraFragment.java: 管理后置摄像头
FrontCameraFragment.java: 管理前置摄像头
ImageProc.java: 定义的Native方法,调用so库
MainActivity.java:统筹管理所有的摄像头
MyRunnable.java:定义的线程接口,用于对数据的处理
UsbCameraFragment.java:管理USB摄像头
(2)jar包、so包
libs目录是存放jar包和so包的。其中libImageProc.so是Native层的编译文件。
(3)布局文件
layout目录是存放对应视图的布局文件。
(4)底层实现
jni目录是存放对USB摄像头的预览和录像支持的文件。
5. 主要功能实现
5.1 Activity对Fragment的动态管理
在MainActivity.java中的初始化方法onCreate中,初始化多个Fragment,并添加到Fragment的事务中,并进行提交。
MainActivity.java
private void initFragment() {
FragmentTransaction transaction = getFragmentManager().beginTransaction();
BackCameraFragment backFragment = new BackCameraFragment();
UsbCameraFragment usbFragment = new UsbCameraFragment();
transaction.add(R.id.back_camera, backFragment);
transaction.add(R.id.usb_camera, usbFragment);
transaction.commitAllowingStateLoss();
}
在activity的布局文件中,对Fragment预留出空的布局,并把Fragment的布局加入。
Activity_main.xml
<FrameLayout
android:id="@+id/back_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1">
<FrameLayout
android:id="@+id/back_camera"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
... ...
动态地添加Fragment,而不是直接在布局文件中定义,这样的好处是便于日后对于其他Fragment的添加,减少代码重构的问题。
而对于各个fragment内的布局,必须实现 onCreateView() 回调方法,Android 系统会在片段需要绘制其布局时调用该方法。对此方法的实现返回的 View 必须是片段布局的根视图。要想从 onCreateView() 返回布局,可以通过 XML 中定义的布局资源来扩展布局。为帮助执行此操作,onCreateView() 提供了一个 LayoutInflater对象。通过LayoutInflater的inflate方法将资源文件加入到布局中。
其中inflate() 方法带有三个参数:
(1)想要扩展的布局的资源 ID;
(2)将作为扩展布局父项的 ViewGroup。传递 container 对系统向扩展布局的根视图(由其所属的父视图指定)应用布局参数具有重要意义;
(3)指示是否应该在扩展期间将扩展布局附加至 ViewGroup(第二个参数)的布尔值。(在本例中,其值为 false,因为系统已经将扩展布局插入 container — 传递 true 值会在最终布局中创建一个多余的视图组。)
5.2 前后摄像头预览
在Android中有两种实现Camera功能的方法,一种是通过Intent简单快速启动Camera;另一种是重新构造一个Camera。CarRecorder应用是采用的第二种方案。
遵照Android平台Camera的架构,CarRecorder应用需要构造上层的Camera。Camera架构图如下所示:
前后摄像头预览过程大致相同,只是打开的摄像头逻辑不同。下面简要分析一下后置摄像头的预览过程。
(1) 继承SurfaceHolder.Callback接口,并实现其三个方法,surfaceCreated,surfaceChanged,surfaceDestroyed。
(2)在fragment的onCreateView初始化方法中,实例化SurfaceHolder,并且设置SurfaceHolder的类型,设置回调。其中mSurface是预览画面的SurfaceView控件。
BackCameraFragment.java
SurfaceHolder surfaceHolder = mSurface.getHolder();
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
surfaceHolder.addCallback(this);
(3)在surfaceCreated回调方法中,开启Camera,对Camera进行初始化操作。
由于目前此设备默认使用的是Camera HAL3.0,需要通过反射的方法去强制使用HAL1.0,这样才会支持前后摄像头同时预览;设置Camera的预览数据的容器,就是之前实例化的SurfaceHolder;设置预览参数;开启预览。
BackCameraFragment.java
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.d(TAG, "surfaceCreated: ");
try {
Method m = Camera.class.getMethod("openLegacy", int.class, int.class);
mCamera = (Camera) m.invoke(null, Integer.valueOf(0), 0x100);
mCamera.setPreviewDisplay(holder);
Camera.Parameters parameters = mCamera.getParameters();
parameters.setPreviewSize(480, 360);
mCamera.setParameters(parameters);
mCamera.startPreview();
} catch (Exception e) {
e.printStackTrace();
}
}
(4)在页面销毁或者被覆盖时,需要调用对Camera资源进行释放,并将相关变量置空,避免下一次操作时对变量进行影响。
BackCameraFragment.java
@Override
public void onPause() {
super.onPause();
Log.d(TAG, "onPause: ");
if (mCamera != null) {
mCamera.stopPreview();
try {
mCamera.setPreviewDisplay(null);
} catch (Exception e) {
Log.d(TAG, "surfaceDestroyed: " + e.toString());
}
mCamera.release();
mCamera = null;
}
}
参考Android官方文档,构造Camera的流程大致如下:
• Detect and Access Camera - Create code to check for the existence of cameras and request access.
• Create a Preview Class - Create a camera preview class that extends SurfaceView and implements the SurfaceHolder interface. This class previews the live images from the camera.
• Build a Preview Layout - Once you have the camera preview class, create a view layout that incorporates the preview and the user interface controls you want.
• Setup Listeners for Capture - Connect listeners for your interface controls to start image or video capture in response to user actions, such as pressing a button.
• Capture and Save Files - Setup the code for capturing pictures or videos and saving the output.
• Release the Camera - After using the camera, your application must properly release it for use by other applications.
5.3 前后摄像头录像
对于前后摄像头录像,原理相同,下面就简要分析一下后置摄像头录像的原理。
在Android系统多媒体框架中,录像功能是使用的是MediaRecorder进行操作的。对于MediaRecorder的使用流程,在Android官方文档上如下解释:
Initialize a new instance of MediaRecorder with the following calls:
• Set the audio source using setAudioSource(). You'll probably use MIC.
Note: Most of the audio sources (including DEFAULT) apply processing to the audio signal. To record raw audio select UNPROCESSED. Some devices do not support unprocessed input. Call AudioManager.getProperty("PROPERTY_SUPPORT_AUDIO_SOURCE_UNPROCESSED") first to verify it's available. If it is not, try using VOICE_RECOGNITION instead, which does not employ AGC or noise suppression. You can use UNPROCESSED as an audio source even when the property is not supported, but there is no guarantee whether the signal will be unprocessed or not in that case.
• Set the output file format using setOutputFormat().
• Set the output file name using setOutputFile().
• Set the audio encoder using setAudioEncoder().
• Complete the initialization by calling prepare().
Start and stop the recorder by calling start() and stop() respectively.
When you are done with the MediaRecorder instance free its resources as soon as possible by calling release()
MediaRecorder状态机模型如下图所示:
在实现录像功能之前需要申请相应的权限,对应到CarRecorder项目中, 由于项目是基于Android7.0,需要动态申请权限,下面是介绍动态申请权限的文档:
requestPermissions:
void requestPermissions (Activity activity,
String[] permissions,
int requestCode)
Requests permissions to be granted to this application. These permissions must be requested in your manifest, they should not be granted to your app, and they should have protection level #PROTECTION_DANGEROUS dangerous, regardless whether they are declared by the platform or a third-party app.
Normal permissions PROTECTION_NORMAL are granted at install time if requested in the manifest. Signature permissions PROTECTION_SIGNATURE are granted at install time if requested in the manifest and the signature of your app matches the signature of the app declaring the permissions.
If your app does not have the requested permissions the user will be presented with UI for accepting them. After the user has accepted or rejected the requested permissions you will receive a callback reporting whether the permissions were granted or not. Your activity has to implement ActivityCompat.OnRequestPermissionsResultCallback and the results of permission requests will be delivered to its onRequestPermissionsResult(int, String[], int[]) method.
Note that requesting a permission does not guarantee it will be granted and your app should be able to run without having this permission.
This method may start an activity allowing the user to choose which permissions to grant and which to reject. Hence, you should be prepared that your activity may be paused and resumed. Further, granting some permissions may require a restart of you application. In such a case, the system will recreate the activity stack before delivering the result to your onRequestPermissionsResult(int, String[], int[]).
When checking whether you have a permission you should use checkSelfPermission(android.content.Context, String).
Calling this API for permissions already granted to your app would show UI to the user to decided whether the app can still hold these permissions. This can be useful if the way your app uses the data guarded by the permissions changes significantly.
You cannot request a permission if your activity sets noHistory to true in the manifest because in this case the activity would not receive result callbacks including onRequestPermissionsResult(int, String[], int[]).
根据官方文档上的说明,在MainActivity中的初始化方法里,对权限进行了动态申请。在BackCameraFragment中的prepareVideoRecorder()方法对MediaRecorder安照官方文档进行了相关的配置,例如设置录像的资源、输出地址、设置预览数据的承载等。在prepareVideoRecorder()的开始必须要先调用Camera的unlock方法,这样MediaRecorder才可以调用Camera的资源。
在结束录像时,需要对MediaRecorder进行重置、释放资源,并且恢复对Camera的上锁状态。
对录像功能总体步骤如下:
- Obtain and initialize a Camera and start preview as described above.
- Call unlock() to allow the media process to access the camera.
- Pass the camera to setCamera(Camera). See MediaRecorder information about video recording.
- When finished recording, call reconnect() to re-acquire and re-lock the camera.
- If desired, restart preview and take more photos or videos.
- Call stopPreview() and release() as described above.
5.4支持USB摄像头
实现USB摄像头之前需要确认其视频节点,具体的设置需要根据当前有几个摄像头来决定,例如,当只有后置摄像头时,USB摄像头的视频节点为video1。更改视频节点的位置,需要修改video_process.cpp文件:
Jni\video_process.cpp
int video_preview_init(void)
{
int ret;
if (TRUE == is_preview)
{
return -100;
}
camera = NULL;
camera = (struct usb_camera *)calloc(1, sizeof(struct usb_camera));
ERROR(NULL == camera, err0, "calloc struct usb_camera\n");
fmt = find_video_format(VIDEO_640x480_MJPEG_TO_MJPEG);
ERROR(NULL == fmt, err1, "Invalid video format\n");
camera->device_name = "/dev/video1";
camera->width = fmt->width;
camera->height = fmt->height;
camera->pixelformat = fmt->in_fmt;
... ...
具体实现USB摄像头功能是在libs的so库中,由jni文件夹中的文件编译而成的,运行命令"<path-to-ndk>/ndk-build NDK_PROJECT_PATH=.",需要ndk的支持。
(1)预览
USB摄像头预览大致的实现方法是:取得Camera每一帧的像素数据,加载到从java层传来的Bitmap对象上,之后将此Bitmap对象回传给java层,通过Canvas画布进行渲染,最后将图像显示到SurfaceView上。在java层处理每一帧数据时,需要开启子线程处理。
下图是其预览实现的设计架构:
(2)录像
CarRecorder应用USB摄像头录制视频是采用的UVC_MJPEG框架,直接用V4L2-API读取帧数据,刷显示,多线程MJPEG4编码、avilib录制。avi只是一个封装格式,里面的视频,可以是任何格式。avi mkv mp4都是封装格式,里面可以有视频流,音频流,字幕流。h264 MJPEG YUV是视频流编码格式。封装格式和视频流编码格式是两个不同的概念。所以avi视频文件里面的视频流可以是h264编码,也可以是MJPEG编码,也可以是YUV编码。考虑在多线程机制下,临界资源保护时,如果对于资源的处理过程很耗时,那么这个处理过程不应该放在保护区内。CarRecorder采用的是,在临界区将资源拷贝一份,离开临界区后再去处理资源,比如涉及到编解码过程,或者写avi文件。典型的,需要用空间换时间的策略。
大致过程直接把MJPEG流写入AVI文件,中间过程不需要编解码操作,这样比较节省资源,只是MJPEG编码 压缩率没h264那么高。
下图是其实现的架构设计: