车载智能终端解决方案

在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相对应的生命周期总结:

activity与fragment相对应的生命周期

对应的效果图如下:

效果图

4. 目录结构

(1)java类文件

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)底层实现

so库代码

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架构图如下所示:

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状态机模型如下图所示:

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的上锁状态。

对录像功能总体步骤如下:

  1. Obtain and initialize a Camera and start preview as described above.
  1. Call unlock() to allow the media process to access the camera.
  2. Pass the camera to setCamera(Camera). See MediaRecorder information about video recording.
  3. When finished recording, call reconnect() to re-acquire and re-lock the camera.
  4. If desired, restart preview and take more photos or videos.
  5. 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那么高。

下图是其实现的架构设计:

录像
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容