需求
实现一个无预览页面,在后台推流、录像、拍照等操作的APP应用。
方案分析
客户需求看似非常简单一句话,实际却是一句非常完美的将程序员产出无限压榨的标准的资本家日常话术。在这么极简的需求面前,是个程序员都不好意思把工作量报成一个月。
好吧!废话不多说,我们来分析一下就这个需求需要实现或者优化哪些技术点。
- 毋庸置疑,第一个肯定是无预览获取camera数据;
- 由于获取的是原始数据,所以我们需要进行编码(包含video和audio);
- 由于我们要实现录制、推流、拍照等,所以需要对编码数据进行封装(如flv、mp4、jpeg等);
- 最后进行录制、推流、拍照等操作;
实践
1、无预览获取camera数据
一开始想无预览还不简单,直接把用于渲染的SurfaceView、GLSurfaceView、TextureView等控件设置1像素不就可以了,想要实现后台偷偷打开,弄个1像素悬浮窗就好了。
但这么弄客户就不乐意了,原因一是得申请权限、二担心耗电、三还有1像素view心里肯定不舒服。
最后通过去翻了官网API、问度娘、问谷歌、问群里大佬得知MediaCodec有个createInputSurface可以获取一个无界面的Surface,把这个塞给camera不就好了(CSDN上有大佬证实过可行性)。考虑到实际应用场景中,不一定都能支持硬解,但看到这里就可以照葫芦画瓢了,自己new一个SurfaceTexture一样塞给camera,通过监听onPreviewFrame获取YUV数据就可以了。
//new SurfaceTexture
SurfaceTexture surfaceTexture = new SurfaceTexture(0);
//打开摄像头
Camera mCamera = Camera.open(mCamId);
//设置camera参数
Camera.Parameters params = mCamera.getParameters();
params.setZoom(0);
params.setPreviewFormat(ImageFormat.NV21);
params.setPreviewSize(width, height);
params.setPictureSize(width, height);
、、、、、、
mCamera.setParameters(params);
//设置回调用于获取摄像头数据
mCamera.setPreviewCallback(mPreviewCallback );
try {
//这一步是最关键的,使用surfaceTexture来承载相机的预览,而不需要设置一个可见的view
mCamera.setPreviewTexture(surfaceTexture);
mCamera.startPreview();
} catch (IOException ioe) {
ioe.printStackTrace();
}
监听回调方法获取摄像头数据,再进行编码、录制等操作
final Camera.PreviewCallback mPreviewCallback = new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
//进行编码、录像等操作
}
};
2、编码、推流、录制
推流、编码常用的也就是MediaCodec、FFmpeg、librtmp,赶项目的程序员都不会重复造轮子,我这里借鉴的rtmp-rtsp-stream-client-java。
3、问题点
一开始没有用rtmp-rtsp-stream-client-java来改,自己DIY的编解码和推流,出现角度问题没法通过setDisplayOrientation来调整角度,尝试对onPreviewFrame返回的YUV调整角度后再进行推流,但出现四重影问题难解决,后面借鉴rtmp-rtsp-stream-client-java,封装好的角度调整,可以完美解决,不过直接在rtmp-rtsp-stream-client-java改更加方便。
public class YUVUtil {
public static void preAllocateBuffers(int length) {
NV21Utils.preAllocateBuffers(length);
YV12Utils.preAllocateBuffers(length);
}
public static byte[] NV21toYUV420byColor(byte[] input, int width, int height,
FormatVideoEncoder formatVideoEncoder) {
switch (formatVideoEncoder) {
case YUV420PLANAR:
return NV21Utils.toI420(input, width, height);
case YUV420SEMIPLANAR:
return NV21Utils.toNV12(input, width, height);
default:
return null;
}
}
public static byte[] rotateNV21(byte[] data, int width, int height, int rotation) {
switch (rotation) {
case 0:
return data;
case 90:
return NV21Utils.rotate90(data, width, height);
case 180:
return NV21Utils.rotate180(data, width, height);
case 270:
return NV21Utils.rotate270(data, width, height);
default:
return null;
}
}
public static byte[] YV12toYUV420byColor(byte[] input, int width, int height,
FormatVideoEncoder formatVideoEncoder) {
switch (formatVideoEncoder) {
case YUV420PLANAR:
return YV12Utils.toI420(input, width, height);
case YUV420SEMIPLANAR:
return YV12Utils.toNV12(input, width, height);
default:
return null;
}
}
public static byte[] rotateYV12(byte[] data, int width, int height, int rotation) {
switch (rotation) {
case 0:
return data;
case 90:
return YV12Utils.rotate90(data, width, height);
case 180:
return YV12Utils.rotate180(data, width, height);
case 270:
return YV12Utils.rotate270(data, width, height);
default:
return null;
}
}
public static Bitmap frameToBitmap(Frame frame, int width, int height, int orientation) {
int w = (orientation == 90 || orientation == 270) ? height : width;
int h = (orientation == 90 || orientation == 270) ? width : height;
int[] argb = NV21Utils.toARGB(rotateNV21(frame.getBuffer(), width, height, orientation), w, h);
return Bitmap.createBitmap(argb, w, h, Bitmap.Config.ARGB_8888);
}
public static byte[] ARGBtoYUV420SemiPlanar(int[] input, int width, int height) {
/*
* COLOR_FormatYUV420SemiPlanar is NV12
*/
final int frameSize = width * height;
byte[] yuv420sp = new byte[width * height * 3 / 2];
int yIndex = 0;
int uvIndex = frameSize;
int a, R, G, B, Y, U, V;
int index = 0;
for (int j = 0; j < height; j++) {
for (int i = 0; i < width; i++) {
a = (input[index] & 0xff000000) >> 24; // a is not used obviously
R = (input[index] & 0xff0000) >> 16;
G = (input[index] & 0xff00) >> 8;
B = (input[index] & 0xff) >> 0;
// well known RGB to YUV algorithm
Y = ((66 * R + 129 * G + 25 * B + 128) >> 8) + 16;
U = ((-38 * R - 74 * G + 112 * B + 128) >> 8) + 128;
V = ((112 * R - 94 * G - 18 * B + 128) >> 8) + 128;
// NV21 has a plane of Y and interleaved planes of VU each sampled by a factor of 2
// meaning for every 4 Y pixels there are 1 V and 1 U. Note the sampling is every other
// pixel AND every other scanline.
yuv420sp[yIndex++] = (byte) ((Y < 0) ? 0 : ((Y > 255) ? 255 : Y));
if (j % 2 == 0 && index % 2 == 0) {
yuv420sp[uvIndex++] = (byte) ((V < 0) ? 0 : ((V > 255) ? 255 : V));
yuv420sp[uvIndex++] = (byte) ((U < 0) ? 0 : ((U > 255) ? 255 : U));
}
index++;
}
}
return yuv420sp;
}
public static byte[] CropYuv(int src_format, byte[] src_yuv, int src_width, int src_height,
int dst_width, int dst_height) {
byte[] dst_yuv;
if (src_yuv == null) return null;
// simple implementation: copy the corner
if (src_width == dst_width && src_height == dst_height) {
dst_yuv = src_yuv;
} else {
dst_yuv = new byte[(int) (dst_width * dst_height * 1.5)];
switch (src_format) {
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar: // I420
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedPlanar: // YV12
{
// copy Y
int src_yoffset = 0;
int dst_yoffset = 0;
for (int i = 0; i < dst_height; i++) {
System.arraycopy(src_yuv, src_yoffset, dst_yuv, dst_yoffset, dst_width);
src_yoffset += src_width;
dst_yoffset += dst_width;
}
// copy u
int src_uoffset = 0;
int dst_uoffset = 0;
src_yoffset = src_width * src_height;
dst_yoffset = dst_width * dst_height;
for (int i = 0; i < dst_height / 2; i++) {
System.arraycopy(src_yuv, src_yoffset + src_uoffset, dst_yuv, dst_yoffset + dst_uoffset,
dst_width / 2);
src_uoffset += src_width / 2;
dst_uoffset += dst_width / 2;
}
// copy v
int src_voffset = 0;
int dst_voffset = 0;
src_uoffset = src_width * src_height + src_width * src_height / 4;
dst_uoffset = dst_width * dst_height + dst_width * dst_height / 4;
for (int i = 0; i < dst_height / 2; i++) {
System.arraycopy(src_yuv, src_uoffset + src_voffset, dst_yuv, dst_uoffset + dst_voffset,
dst_width / 2);
src_voffset += src_width / 2;
dst_voffset += dst_width / 2;
}
}
break;
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar: // NV12
case MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420PackedSemiPlanar: // NV21
case MediaCodecInfo.CodecCapabilities.COLOR_TI_FormatYUV420PackedSemiPlanar:
case MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar: {
// copy Y
int src_yoffset = 0;
int dst_yoffset = 0;
for (int i = 0; i < dst_height; i++) {
System.arraycopy(src_yuv, src_yoffset, dst_yuv, dst_yoffset, dst_width);
src_yoffset += src_width;
dst_yoffset += dst_width;
}
// copy u and v
int src_uoffset = 0;
int dst_uoffset = 0;
src_yoffset = src_width * src_height;
dst_yoffset = dst_width * dst_height;
for (int i = 0; i < dst_height / 2; i++) {
System.arraycopy(src_yuv, src_yoffset + src_uoffset, dst_yuv, dst_yoffset + dst_uoffset,
dst_width);
src_uoffset += src_width;
dst_uoffset += dst_width;
}
}
break;
default: {
dst_yuv = null;
}
break;
}
}
return dst_yuv;
}
}
实现效果
手机端,开始推流后直接回到桌面任可以继续推流;
SRS播放端,演示在1秒以内;