前面讲到了在Android平台下使用FFmpeg进行RTMP推流(视频文件推流),里面主要是介绍如何解析视频文件并进行推流,今天要给大家介绍如何在Android平台下获取采集的图像,并进行编码推流。同时项目工程也是在之前的代码基础上新增功能。源码仓库地址FFmpegSample,这一节对应的代码版本是v1.2。大家注意不要下载错了版本。主要涉及的代码。
建议:这套代码和讲解中,有些地方我也还没研究透彻,但这个不影响我们要实现的功能,我之前也特别纠结一些细节,花了很多的时间。其实学习一门技术和框架是一个慢慢深入的过程,刚开始我们先跑起来,再深入,否则如果你还没入门,就开始纠结一些细节参数,然后又发现网上很难找到答案,那你的自信心就会受到打击,这也是我自己的体验,和大家分享一下。等到我们越来越熟悉FFmpeg和一些技术,那么之前的问题都会迎刃而解
这套代码我在4.4.2上运行时没问题的。所以如果有同学在5.0以上,如果涉及动态权限问题,大家加上即可。学习本章之前最好先看之前的文章,这里是一套连贯的教程
- RTMP服务器搭建(crtmpserver和nginx)
- 音视频编码相关名词详解
- 基于FFmpeg进行RTMP推流(一)
- 基于FFmpeg进行RTMP推流(二)
- Linux下FFmpeg编译以及Android平台下使用
- Android平台下使用FFmpeg进行RTMP推流(视频文件推流)
打开摄像头并设置参数
具体代码查看CameraActivity.java
private Camera getCamera() {
Camera camera;
try {
//打开相机,默认为后置,可以根据摄像头ID来指定打开前置还是后置
camera = Camera.open(1);
if (camera != null && !isPreview) {
try {
Camera.Parameters parameters = camera.getParameters();
//对拍照参数进行设置
for (Camera.Size size : parameters.getSupportedPictureSizes()) {
LogUtils.d(size.width + " " + size.height);
}
LogUtils.d("============");
for (Camera.Size size : parameters.getSupportedPreviewSizes()) {
LogUtils.d(size.width + " " + size.height);
}
parameters.setPreviewSize(screenWidth, screenHeight); // 设置预览照片的大小
parameters.setPreviewFpsRange(30000, 30000);
parameters.setPictureFormat(ImageFormat.NV21); // 设置图片格式
parameters.setPictureSize(screenWidth, screenHeight); // 设置照片的大小
camera.setParameters(parameters);
//指定使用哪个SurfaceView来显示预览图片
camera.setPreviewDisplay(sv.getHolder()); // 通过SurfaceView显示取景画面
camera.setPreviewCallback(new StreamIt()); // 设置回调的类
camera.startPreview(); // 开始预览
//Camera.takePicture()方法进行拍照
camera.autoFocus(null); // 自动对焦
} catch (Exception e) {
e.printStackTrace();
}
isPreview = true;
}
} catch (Exception e) {
camera = null;
e.printStackTrace();
Toast.makeText(this, "无法获取前置摄像头", Toast.LENGTH_LONG);
}
return camera;
}
Camera.open(int cameraId)
这里是创建一个Camera对象对应具体的硬件摄像头,如果摄像头已经被其他app打开,就会抛出RuntimeException异常。
cameraId是camera的Id。我们可以通过getNumberOfCameras()
获取摄像头的数量,那id的范围就是0~(getNumberOfCameras()-1)。一般情况下传0就直接获取到后置摄像头,1就获取到前置摄像头。当然有些设备可能有些不同。
Camera.Parameters
这个类用于存储和设置摄像头的参数信息,当然Camera有很多默认参数,所以我们只需要通过camera.getParameters()
获取该对象,然后并设置我们需要修改的属性即可。我们看一些常见的属性设置
-
setPreviewSize
设置预览图像的大小
-
setPictureSize
设置照片的大小
-
setPreviewFpsRange
设置Fps,帧率。但我发现并没有什么卵用。每次修改后采集的频率还是没变,擦!
-
setPictureFormat
设置采集到图像的像素格式,Android推荐NV21。那我们就用这个,这个参数很重要,后面编码我们会详细讲解。
最后不要忘了调用setParameters
进行设置。否则你就白忙活了。
预览和获取采集图像数据
预览
第一个问题,用什么来承载预览图像。Android提供了SurfaceView和GLSurfaceView。这里为了方便大家上手,我们先选择使用SurfaceView稍微简单一点,对SurfaceView大家不熟的可以查找相关资料。接下来就是使用SurfaceView
-
布局中添加SurfaceView。这里我做了一个继承类
MySurfaceView
<com.wangheart.rtmpfile.MySurfaceView android:id="@+id/sv" android:layout_width="match_parent" android:layout_height="match_parent" />
-
获取SurfaceHolder并设置回调
SurfaceView里有一个SurfaceHolder用来控制SurfaceView的相关操作。比如设置SurfaceView的Callback,用来监听SurfaceView的创建,变化和销毁。这里只需要实现
SurfaceHolder.Callback
的接口@Override public void surfaceCreated(SurfaceHolder holder) { setStartPreview(mCamera, mHolder); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { setStartPreview(mCamera, mHolder); } @Override public void surfaceDestroyed(SurfaceHolder holder) { releaseCamera(); }
然后设置到SurfaceHolder中
mHolder.addCallback(this)
-
SurfaceView与Camera关联
因为我们要讲图像预览到SurfaceView上,那么必定有地方存在关联。这里很简单,就是调用Camera的
setPreviewDisplay
,将SurfaceView的SurfaceHolder设置进去即可。 -
开始预览
直接调用camera的
startPreview
开始进行预览。那么什么时候调用这个方法呢?- 设置一个按钮,点击之后我们就调用这个方法进行预览
- SurfaceView的创建回调方法中
surfaceCreated
中进行调用,因为图像要预览到SurfaceView中,所以必须得SurfaceView已成功创建。
获取采集数据
前面我们已经知道怎么预览图像了。接下来就是获取采集数据。这个也很容易就是调用Camera的setPreviewCallback
设置预览回调。我们实现一下这个接口
public class StreamIt implements Camera.PreviewCallback {
@Override
public void onPreviewFrame(final byte[] data, Camera camera) {
long endTime = System.currentTimeMillis();
executor.execute(new Runnable() {
@Override
public void run() {
encodeTime = System.currentTimeMillis();
FFmpegHandle.getInstance().onFrameCallback(data);
LogUtils.w("编码第:" + (encodeCount++) + "帧,耗时:" + (System.currentTimeMillis() - encodeTime));
}
});
LogUtils.d("采集第:" + (++count) + "帧,距上一帧间隔时间:"
+ (endTime - previewTime) + " " + Thread.currentThread().getName());
previewTime = endTime;
}
}
很简单,这个接口就是讲原始数据进行回调。这里大家也看到了,我把采集的时间间隔和编码消耗的时间打印出来了。
编码
前面把基础的如何采集摄像头数据讲了一下,接下来就是进行视频数据编码。
开启线程编码
因为编码毕竟会比较耗时,所以我们需要放到线程中处理,这里我用了一个单线程池,避免每次开启和销毁线程产生的开销。为了保证图片按顺序编码,这里使用单线程池。
ExecutorService executor = Executors.newSingleThreadExecutor();
获取到采集的数据后就可以丢进去进行编码
executor.execute(new Runnable() {
@Override
public void run() {
encodeTime = System.currentTimeMillis();
FFmpegHandle.getInstance().onFrameCallback(data);
LogUtils.w("编码第:" + (encodeCount++) + "帧,耗时:" + (System.currentTimeMillis() - encodeTime));
}
});
这里大家也看出来了调用FFmpegHandle.getInstance().onFrameCallback(data);
进行编码。
初始化编码相关操作
这里我们使用的是FFmpeg,所以在编码前我们会先做一些初始化以及参数设置工作,所以我们在FFmpegHandle中增加一个native方法public native int initVideo(String url);
对应到C++层,也就是ffmpeg_handle.cpp
AVFormatContext *ofmt_ctx;
AVStream *video_st;
AVCodecContext *pCodecCtx;
AVCodec *pCodec;
AVPacket enc_pkt;
AVFrame *pFrameYUV;
int count = 0;
int yuv_width;
int yuv_height;
int y_length;
int uv_length;
int width = 480;
int height = 320;
int fps = 15;
/**
* 初始化
*/
extern "C"
JNIEXPORT jint JNICALL
Java_com_wangheart_rtmpfile_ffmpeg_FFmpegHandle_initVideo(JNIEnv *env, jobject instance,
jstring url_) {
const char *out_path = env->GetStringUTFChars(url_, 0);
logd(out_path);
//计算yuv数据的长度
yuv_width = width;
yuv_height = height;
y_length = width * height;
uv_length = width * height / 4;
av_register_all();
//output initialize
avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_path);
//output encoder initialize
pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!pCodec) {
loge("Can not find encoder!\n");
return -1;
}
pCodecCtx = avcodec_alloc_context3(pCodec);
//编码器的ID号,这里为264编码器,可以根据video_st里的codecID 参数赋值
pCodecCtx->codec_id = pCodec->id;
//像素的格式,也就是说采用什么样的色彩空间来表明一个像素点
pCodecCtx->pix_fmt = AV_PIX_FMT_YUV420P;
//编码器编码的数据类型
pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
//编码目标的视频帧大小,以像素为单位
pCodecCtx->width = width;
pCodecCtx->height = height;
pCodecCtx->framerate = (AVRational) {fps, 1};
//帧率的基本单位,我们用分数来表示,
pCodecCtx->time_base = (AVRational) {1, fps};
//目标的码率,即采样的码率;显然,采样码率越大,视频大小越大
pCodecCtx->bit_rate = 400000;
//固定允许的码率误差,数值越大,视频越小
// pCodecCtx->bit_rate_tolerance = 4000000;
pCodecCtx->gop_size = 50;
/* Some formats want stream headers to be separate. */
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
pCodecCtx->flags |= CODEC_FLAG_GLOBAL_HEADER;
//H264 codec param
// pCodecCtx->me_range = 16;
//pCodecCtx->max_qdiff = 4;
pCodecCtx->qcompress = 0.6;
//最大和最小量化系数
pCodecCtx->qmin = 10;
pCodecCtx->qmax = 51;
//Optional Param
//两个非B帧之间允许出现多少个B帧数
//设置0表示不使用B帧
//b 帧越多,图片越小
pCodecCtx->max_b_frames = 0;
// Set H264 preset and tune
AVDictionary *param = 0;
//H.264
if (pCodecCtx->codec_id == AV_CODEC_ID_H264) {
// av_dict_set(¶m, "preset", "slow", 0);
/**
* 这个非常重要,如果不设置延时非常的大
* ultrafast,superfast, veryfast, faster, fast, medium
* slow, slower, veryslow, placebo. 这是x264编码速度的选项
*/
av_dict_set(¶m, "preset", "superfast", 0);
av_dict_set(¶m, "tune", "zerolatency", 0);
}
if (avcodec_open2(pCodecCtx, pCodec, ¶m) < 0