近期做了一段时间的Camera的开发,虽然是应用层的开发但是也粗略的接触了一些framework相关的东西,在此写出来分享下。
先说下应用开发的目的和实用环境,这次开发的主要目的是实现android设备上外挂USB摄像头来实现录像拍照的功能,类似行车记录仪的功能,但是灵活性更大因为可随时在android设备上回看录像和回看照片,并可查看GPS的运行轨迹,当然主要功能还是录像和拍照功能了。</p><p> 首先还是要做一个简单的demo功能出来,后面再逐渐的去丰富各个功能模块和完善BUG。
第一步肯定是建工程了,一个主Activity用来显示录像预览的图像,并且设计简单的按钮分别来实现开始关闭录像,拍照,浏览录像和照片等功能。 当然首先还是要在AndroidManifest.xml中加入
<uses-permission android:name = "android.permission.CAMERA" />
<uses-feature android:name = "android.hardware.camera" />
<uses-feature android:name = "android.hardware.camera.autofocus" />
<uses-permission android:name = "android.permission.RECORD_AUDIO"></span>
之所以加入RECORD_AUDIO是因为后期可能会加入声音录入,因此提前加进来。
主Activity需要实现OnClickListener,SurfaceHolder.Callback,OnCheckChangeListener等接口,因为所设计的按钮分别需要监听Click,录像模式选择会监听CheckBox的动作。
核心代码
下面的函数主要是实现Camera取景预览的界面,并且初始化了应用的初始界面,包括按钮和预览界面。
取景的容器为SurfaceView,使用它必须用到SurfaceHolder,它可以随时监听Surface的变化。并且需要将SurfaceHolder的类型设置为SURFACE_TYPE_PUSH_BUFFERS.这样画面缓存会由Camera类来管理。
private void initView() {
display = (SurfaceView) findViewById(R.id.display);
surfaceHolder = display.getHolder();
surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
camera = (ImageButton) findViewById(R.id.camera);
video_record = (CheckBox) findViewById(R.id.video);
camera_shoot = (ImageButton) findViewById(R.id.shoot);
explorer = (Button) findViewById(R.id.explorer);
settings = (Button) findViewById(R.id.settings);
surfaceHolder.addCallback(this);
surfaceHolder.setFormat(PixelFormat.TRANSPARENT);
surfaceHolder.setFixedSize(1280,720);
camera.setOnClickListener(this);
camera_shoot.setOnClickListener(this);
explorer.setOnClickListener(this);
settings.setOnClickListener(this);
timer = (TextView)findViewById(R.id.timer);
video_record.setOnClickListener(this);
}
Camera录像拍照等功能我实现均在另一个Service中,目的也是可以实现后台录像的功能,并随时监听操作。
核心代码
此处代码主要是完成录像画面的预览,其中会进行USB设备的热插拔监听,拍照Camera属性设置已经Camera对象的初始化,打开预览等操作。
public void surfaceCreate() throws RemoteException {
Log.e(TAG, "surfaceCreate");
mSurfaceHolder = application.getSurfaceHolder();
handler.removeCallbacks(USBtask);
handler.postDelayed(USBtask, 5000);
try {
if(!screen_on){
screen_on = true;
handler.sendEmptyMessage(GPS_INTERVAL);
handler.sendEmptyMessageDelayed(RESUME, 3000);
return;
}
if(mCamera == null){
mCamera = Camera.open(cameraId);
cameraPictureParamsSet();
mCamera.setPreviewDisplay(mSurfaceHolder);
startPreview();
Log.e(TAG, "surfaceCreate mCamera is null, startPreview!");
if(isRecording){
cameraListener.autoStartRecord();
startRecord();
}
}else{
if(isRecording)
cameraListener.autoStartRecord();
mCamera.setPreviewDisplay(mSurfaceHolder);
startPreview();
Log.e(TAG, "surfaceCreate mCamera is not null, startPreview!");
}
} catch (IOException e) {
e.printStackTrace();
}catch(RuntimeException e){
cameraListener.noCamera();
}
}
这部分代码就是录像的主要代码了,其中主要涉及到了MediaRecorder的部分操作,包括它输出视频流保存的格式和压缩方式,码率和码流,分辨率等参数的设置。因此处较易出现异常因此建议多catch异常以免程序崩溃。
private void recordstart(boolean tts){
hour = 0;
minute = 0;
second = 0;
if (mMediaRecorder == null){
mMediaRecorder = new MediaRecorder();
}else{
mMediaRecorder.reset();
try {
mCamera.reconnect();
} catch (IOException e1) {
e1.printStackTrace();
}
}
cameraPictureParamsSet();
mCamera.unlock();
if(mSurfaceHolder == null)
mCamera.stopPreview();
mMediaRecorder.setCamera(mCamera);
boolean record_toggle = preference.getBoolean("record", true);
if(record_toggle){
mMediaRecorder
.setAudioSource(MediaRecorder.AudioSource.MIC);
}
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mMediaRecorder.setVideoFrameRate(mVideoFrameRate);
mMediaRecorder.setVideoSize(1280, 720);
mMediaRecorder.setVideoEncodingBitRate(4000000);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
if(record_toggle){
mMediaRecorder
.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mMediaRecorder.setAudioChannels(1);
mMediaRecorder.setAudioSamplingRate(8000);
}
mMediaRecorder.setMaxDuration(max_duration_ms);
mMediaRecorder.setOnErrorListener(CameraService.this);
mMediaRecorder.setOnInfoListener(CameraService.this);
getVideoName();
if(isPathValid){
try {
recordingPath = getVideoName().getAbsolutePath();
mMediaRecorder.setOutputFile(recordingPath);
fileUtils.setRecordingfile(recordingPath);
mMediaRecorder.prepare();
mMediaRecorder.start();
handler.removeCallbacks(task);
if(isPathValid){
handler.postDelayed(task, 1000);
}
cameraListener.autoStartRecord();
if(tts)
tts(getString(R.string.isrecodering));
isRecording = true;
} catch (IllegalStateException e) {
recordFail();
}catch(IOException e){
recordFail();
} catch (RemoteException e) {
e.printStackTrace();
}catch(RuntimeException e){
recordFail();
}
}else{
handler.removeCallbacks(task);
recordFail();
}
}
private void releaseRecord(){
mMediaRecorder.setOnErrorListener(null);
mMediaRecorder.setOnInfoListener(null);
mMediaRecorder.release();
mMediaRecorder = null;
handler.removeCallbacks(task);
fileUtils.setRecordingfile("");
}
public void recordStop(){
try {
isRecording = false;
mMediaRecorder.stop();
mMediaRecorder.reset();
//mCamera.lock();
mCamera.reconnect();
releaseRecord();
mCamera.setTrechometer(mTrechometer);
mTrechometer++;
mCamera.setCoordinate(1, mCoordinate1, mCoordinate2);
mCoordinate1++;
mCoordinate2 = mCoordinate2 + 1*60;
} catch (Exception e) {
e.printStackTrace();
}
}
public void takephoto(){
try {
mCamera.autoFocus(null);
} catch (RuntimeException e) {
e.printStackTrace();
}
mCamera.takePicture(new ShutterCallback() {
@Override
public void onShutter() {
}
}, null, pictureCallback);
}
以上差不多是程序里比较核心和重要的代码了,因为工作原因无法公开全部代码。但是大概流程还是很清楚的,在此重新总结下就是开发者指南中的流程
1. 打开摄像头 —— 用Camera.open() 来获得一个camera对象的实例。
2. 连接预览 —— 用Camera.setPreviewDisplay()将camera连接到一个SurfaceView ,准备实时预览。
3. 开始预览 —— 调用 Camera.startPreview() 开始显示实时摄像画面。
4. 开始录制视频 —— 严格按照以下顺序执行才能成功录制视频:
a. 解锁Camera —— 调用Camera.unlock()解锁,便于MediaRecorder 使用摄像头。
b. 配置MediaRecorder —— 按照如下顺序调用MediaRecorder 中的方法。详情请参阅MediaRecorder 参考文档。
1. setCamera() —— 用当前Camera实例将摄像头用途设置为视频捕捉。
2. setAudioSource() —— 用MediaRecorder.AudioSource.CAMCORDER设置音频源。
3. setVideoSource() —— 用MediaRecorder.VideoSource.CAMERA设置视频源。
4. 设置视频输出格式和编码格式。对于Android 2.2 (API Level 8) 以上版本,使用MediaRecorder.setProfile方法,并用CamcorderProfile.get()来获取一个profile实例。对于Android prior to 2.2以上版本,必须设置视频输出格式和编码参数:
i. setOutputFormat() —— 设置输出格式,指定缺省设置或MediaRecorder.OutputFormat.MPEG_4。
ii. setAudioEncoder() —— 设置声音编码类型。指定缺省设置或MediaRecorder.AudioEncoder.AMR_NB。
iii. setVideoEncoder() —— 设置视频编码类型,指定缺省设置或者MediaRecorder.VideoEncoder.MPEG_4_SP。
5. setOutputFile() —— 用getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()设置输出文件,见保存媒体文件一节中的方法示例。
6. setPreviewDisplay() —— 用上面连接预览中设置的对象来指定应用程序的SurfaceView 预览layout元素。
警告: 必须按照如下顺序调用MediaRecorder的下列配置方法,否则应用程序将会引发错误,录像也将失败。
c. 准备MediaRecorder —— 调用MediaRecorder.prepare()设置配置,准备好MediaRecorder 。
d. 启动MediaRecorder —— 调用MediaRecorder.start()开始录制视频。
5. 停止录制视频 —— 按照顺序调用以下方法,才能成功完成视频录制:
a. 停止MediaRecorder —— 调用MediaRecorder.stop()停止录制视频。
b. 重置MediaRecorder —— 这是可选步骤,调用MediaRecorder.reset()删除recorder中的配置信息。
c. 释放MediaRecorder —— 调用MediaRecorder.release()释放MediaRecorder。
d. 锁定摄像头 —— 用Camera.lock()锁定摄像头,使得以后MediaRecorder session能够使用它。自Android 4.0 (API level 14)开始,不再需要本调用了,除非MediaRecorder.prepare()调用失败。
6. 停止预览 —— activity使用完摄像头后,应用Camera.stopPreview()停止预览。
7. 释放摄像头 —— 使用Camera.release()释放摄像头,使其它应用程序可以使用它。
最后说下开发中遇到的比较多的问题:
1.首先来说是USB摄像头的选择,由于录像所占用的CPU资源和内存资源会相对较大,因此系统资源会显得紧张,设备变慢运行卡顿等问题可能会在某些配置较低的设备上出现,所以要选择的摄像头应该尽可能适应于当前设备的配置,如果清晰度很高则会出现视频采样到压缩保存过程系统无法满足甚至是存储设备速率无法满足造成录制的视频花屏异常。
2.因此录像需要保存,因此在保存过程中会反复的写存储设备,无论是SD卡还是TF卡还是U盘,都将要求有较高的稳定性及读写速度,很简单的一个例子就是我所用的TF卡最开始为class 4的金士顿,这个过程可能因为卡的质量不良会经常出现写数据导致的卡内自动写保护,进而导致了程序录像停止甚至弹错退出。因此录像存储这个地方的设备选择需要考虑,可能换class10的卡会效果更好,但是需要考虑成本,另外也可以通过降低录像的视频帧率来减小录像的大小。
3.因为是外接USB摄像头,所以对于USB的插拔需要随时监听以免出现设备已经移除了程序却还在运行,进而导致错误崩溃,造成体验不良。
4.录像过程中的lock和unlock操作,因为在录像过程中无法预知会发生什么错误,因此我并未采用对Camera的lock操作,因为我所用的android设备只会外接1个Camera设备,因此不担心多设备和多应用打开同一个摄像头的问题,但是同样有一个问题无法解决,就是当应用因为未知原因出现崩溃的时候,如内存紧张造成的系统回收或者Service异常停止等等。此时的Camera就会无法得到正常的释放,此时问题就出现了,Camera被锁定了,其他程序无法使用,并且此应用重新打开依然无法使用。此问题现在依然未解,只能尽量的减小程序异常崩溃的概率来降低这种问题的出现了。
以上表述有些凌乱,勉强可以看看。由于Camera所涉及的东西从上到下还是有非常多的东西,而我接触的也仅仅是其中的一小部分,仅仅是为了工作而学习了部分有用的内容,后面如果有机会还是应该多深入的了解下更多的东西。最后贴出一部分调试时遇到的log打印和出处。
android\frameworks\av\services\camera\libcameraservice下的CameraService.cpp中,status_t CameraService::Client::checkPid()函数内。贴出代码
status_t CameraService::Client::checkPid() const {
int callingPid = getCallingPid();
if (callingPid == mClientPid) return NO_ERROR;
ALOGW("attempt to use a locked camera from a different process"
" (old pid %d, new pid %d)", mClientPid, callingPid);
return EBUSY;
}
每次插拔USB设备后video的设备号会发生改变,导致无法打开摄像头所以显示无设备。
更新过mstar提供的最新USB的驱动后,修改了如下地方\kernel\drivers\media\video下的v4l2-dev.c中
<span style="font-size:14px;">void video_unregister_device(struct video_device *vdev)
{
/* Check if vdev was ever registered at all */
if (!vdev || !video_is_registered(vdev))
return;
mutex_lock(&videodev_lock);
/* This must be in a critical section to prevent a race with v4l2_open.
* Once this bit has been cleared video_get may never be called again.
*/
clear_bit(V4L2_FL_REGISTERED, &vdev->flags);
devnode_clear(vdev);//add by hzhang
mutex_unlock(&videodev_lock);
device_unregister(&vdev->dev);
}
</span>
加了一句清除设备号的代码,使其设备号不会顺序的改变,从而不会使每次打开的设备号不同而打不开设备。
E/CameraService( 855): mSurface or mPreviewWindow must be set before startRecordingMode.
查到CameraService.cpp(frameworks/av/services/camera/libcameraservice/)
status_t CameraService::Client::startCameraMode函数内
case CAMERA_RECORDING_MODE:
if (mSurface == 0 && mPreviewWindow == 0) {
ALOGE("mSurface or mPreviewWindow must be set before startRecordingMode.");
//return INVALID_OPERATION;
}
注释掉那个返回值,使其在没有预览窗口View时也可以继续开始录像。
以上是部分log和出处,希望对有需要的同学有用。