当前位置:AIGC资讯 > 数据采集 > 正文

视频直播相机采集篇

这是一篇以前的开发笔记,当时5.0以下的系统占比还不少,所以使用了旧的Camera Api。

下面是正文:

虽然从API21开始Google已经推出了一套新的Camera Api,但是鉴于目前还有很多手机运行在Api 21之下,SDK仍使用旧版本的Api。

Andorid系统碎片化比较严重,相应的硬件也是五花八门,所以开发的时候一定做好判断和保护,不然指不定在那台机器上就会出现诡异的crash。

打开摄像头

通过调用Camera.open()即可打开摄像头,不过默认打开的是后置摄像头,如果要指定前后摄像头,需要传入cameraId,如何获取cameraId,来看一下Camera.open()源码中是如何做的:

 /**
 * Creates a new Camera object to access the first back-facing camera on the
 * device. If the device does not have a back-facing camera, this returns
 * null.
 * @see #open(int)
 */
public static Camera open() {
	int numberOfCameras = getNumberOfCameras();
	CameraInfo cameraInfo = new CameraInfo();
	for (int i = 0; i < numberOfCameras; i++) {
		getCameraInfo(i, cameraInfo);
		if (cameraInfo.facing == CameraInfo.CAMERA_FACING_BACK) {
			return new Camera(i);
		}
	}
	return null;
}

遍历所有的摄像头,然后根据CameraInfo的facing来找到后置摄像头的索引,这个索引就是cameraId。之前犯错这样一个错误:把CameraInfo.CAMERA_FACING_BACK当成cameraId来使用,在很多手机上索引值和CameraInfo.CAMERA_FACING_BACK值是一样的,导致很久之后才发现这个bug。

Camera.CameraInfo cameraInfo = new Camera.CameraInfo();

for (int i = 0; i < Camera.getNumberOfCameras(); ++i) {
	Camera.getCameraInfo(i, cameraInfo);

	if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
		mCameraIDFront = i;
	}
	if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_BACK) {
		mCameraIDBack = i;
	}
}

好了,下面来打开摄像头:

Camera.Parameters parameters;
try {
	mCamera = Camera.open(frontCamera ? mCameraIDFront : mCameraIDBack);  
	parameters = mCamera.getParameters();
} catch (Exception e) {
	mCamera = null;
	e.printStackTrace();
	return;
}

对摄像头的操作尽量都要放到try/catch里面,做好容错。因为这些操作失败之后都会抛出crash,不同的机型表现还不一样,防不胜防。

指定相机参数

上面打开摄像头之后我们已经获取到了Camera.Parameters,下一步我们来设置摄像头的预览参数。

闪光灯:

List<String> flashModes = parameters.getSupportedFlashModes();
if (flashModes != null && flashModes.contains(Camera.Parameters.FLASH_MODE_OFF)) {
	parameters.setFlashMode(Camera.Parameters.FLASH_MODE_OFF);
}

要把闪光灯关掉,不然自动对焦的时候闪光灯可能会打开,看下面这段说明:

<p>If the current flash mode is not
{[@link]android.hardware.Camera.Parameters#FLASH_MODE_OFF}, flash may be
fired during auto-focus, depending on the driver and camera hardware.<p>

分辨率:

List<Camera.Size> sizes = parameters.getSupportedPreviewSizes();
for (int i = 0; i < sizes.size(); i++) {
	Camera.Size s = sizes.get(i);
}

目前的策略是允许外部用户从360P/540P/720P三种分辨率中选择一种,如果有指定的分辨率就选择指定的分辨率,如果没有就寻找最接近的分辨率进行裁剪,仅供参考。

FPS:

List<Integer>   fpsList = parameters.getSupportedPreviewFrameRates();      
parameters.setPreviewFrameRate(getSupportedFPS(mFps)); 

也是先获取系统支持的分辨率列表,然后通过getSupportedFPS方法寻找最接近的FPS值

连续自动对焦:

if (focusModes != null && focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
	parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}

这种对焦方式会在视频录制过程中自动根据当前的情况进行对焦,不过从实际测试看,有些手机不支持,就算是支持也只能支持后置摄像头,下篇文章会介绍触摸对焦的实现方式作为补充。

视频去抖动:

String vstabSupported = mParameters.get("video-stabilization-supported");
if ("true".equals(vstabSupported)) {
	parameters.set("video-stabilization", "true");
}

API14以下是没有这个接口的,所以要通过这种方式来设置。

上面这些是一些主要的参数设置,更多的特性可参考: https://developer.android.com/guide/topics/media/camera.html#camera-features

指定采集格式

摄像头采集出来的是YUV格式的数据,官方推荐的两种格式是YV12和NV21,默认格式为NV21,SDK中使用默认格式:

parameters.setPreviewFormat(ImageFormat.NV21)

然后设置数据回调:

//用选定的预览分辨率计算一帧的大小,Y分量数是mWidth*mHeight, UV分量一共占Y分量的一半
mVBuffer = new byte[mWidth*mHeight*3/2];
mCamera.addCallbackBuffer(mVBuffer);
mVBuffer = new byte[mWidth*mHeight*3/2];
mCamera.addCallbackBuffer(mVBuffer);
mVBuffer = new byte[mWidth*mHeight*3/2];
mCamera.addCallbackBuffer(mVBuffer);
mCamera.setPreviewCallbackWithBuffer((Camera.PreviewCallback) fetchVideoFromDevice());

private Object fetchVideoFromDevice() {
	return new Camera.PreviewCallback() {
	@Override
	public void onPreviewFrame(byte[] data, Camera camera) {
		//todo: send data
		camera.addCallbackBuffer(data);
	}
}

上面设置了三个buffer到回调的buffer队列中,Camera会循环利用这三段buffer把数据回调出来,用完之后记得需要还回去。

NV21是4:2:0采样,每四个Y共用一组UV分量:YYYYYYYY VUVU,最终发送的时候需要进行编码,H264编码器一般接受的是I420(YYYYYYYY UU VV)格式的数据,所以把数据送给编码器之前需要转换一下YUV的数据格式:

int wh = width * height;
byte[] yuvData = new byte[(wh * 3)>> 1];
System.arraycopy(data, 0, yuvData, 0, wh);

for (int i = 0, j = 0; i < (wh >> 1); i++) {
	if (i % 2 == 0) {
		yuvData[wh + j + 1] = data[wh + i];
	} else {
		yuvData[wh + j] = data[wh + i];
	}
	j++;
}

上面只是提供一种思路,实际在SDK中这些转换操作都是在C++层和图像的旋转裁剪一起做的。

启动预览

相机传感器的采集角度一般是倒的,就是说,如果你竖着拿手机,那么相机采集出来的图像实际上是倒的。比如下面这张图,左边是传感器的取景框,右边手机的渲染区域,预览角度要正向旋转90度渲染出来的画面才是正的。

根据实际情况来看ios一般是90度,Android的话大部分是90,也有可能是270,看过很多程序里面都固定写死预览角度90,会存在兼容性问题,所以规范的做法是根据当前屏幕的方向和相机传感器的方向确定预览角度,下面这段是官方文档上推荐的计算预览角度的方法。

private int getCameraDisplayOrientation(Activity activity, int cameraId, Camera camera) {
	Camera.CameraInfo info = new Camera.CameraInfo();
	Camera.getCameraInfo(cameraId, info);

	int rotation = activity.getWindowManager().getDefaultDisplay()
		.getRotation();

	int degrees = 0;
	switch (rotation) {
	case Surface.ROTATION_0: degrees = 0; break;
	case Surface.ROTATION_90: degrees = 90; break;
	case Surface.ROTATION_180: degrees = 180; break;
	case Surface.ROTATION_270: degrees = 270; break;
	}

	int result;
	if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
		result = (info.orientation + degrees) % 360;
		result = (360 - result) % 360;  // compensate the mirror
	} else {  // back-facing
		result = (info.orientation - degrees + 360) % 360;
	}

	return result;
}

按照正常流程来说,走到这里就要给相机设置预览角度和设置预览布局:

mCamera.setDisplayOrientation(angle);
mCamera.setPreviewDisplay(cameraView.getHolder());

但是,因为要做美颜,这里并没有使用Camera提供的预览,而是拿到数据之后自己先做美颜再使用OpenGL进行渲染,所以这里设置预览角度是不起作用的。

另外有一点,如果通过Camera来预览,setPreviewDisplay设置的预览角度并不能真正改变图像的角度,不管怎么样最终发送出去的数据都需要根据上面计算的角度来旋转以保证观众看到的图片是正的。

实际发现,如果不调用setPreviewDisplay摄像头会不吐数据出来...,所以一种比较恶心的做法是在屏幕左上角添加一个像素点大小的SurfaceView,设置给Camera:

Handler createH = new Handler(context.getMainLooper());
		createH.postDelayed(new Runnable() {
			@Override
			public void run() {
				synchronized (mInitLock) {
					//set camera preview
					if (mCameraView == null) {
						mCameraView = new SurfaceView(mContext);
						if (getParent() instanceof ViewGroup) {
							ViewGroup.MarginLayoutParams params = new ViewGroup.MarginLayoutParams(1, 1);
							params.leftMargin = -1680;
							((ViewGroup) getParent()).addView(mCameraView, params);
						}
					}
					mInitLock.notify();
				}
			}
		},0);



public SurfaceView getSurfaceView() {
	synchronized (mInitLock) {
		if (mCameraView == null) {
			try {
				mInitLock.wait(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
	return mCameraView;
}

上面用到了wait/notify,目的是在将1个像素的视图添加到Camera之前需要确认这个视图是已经创建成功了的。

如果App不支持3.0以下的版本,可以使用SurfaceTexture:

mSurfaceTexture = new SurfaceTexture(0);

mCamera.setPreviewTexture(mSurfaceTexture);

好了,下面开始正式的启动预览:

try {
	mCamera.setParameters(parameters);
	mCamera.setPreviewDisplay(cameraView.getHolder());
	mCamera.startPreview();
} catch (Exception e) {
	e.printStackTrace();
	return;
}

下面就可以把数据送去美颜、本地渲染、编码、发送了。

更新时间 2023-11-08