视频编码有几种方式:
1.硬编码,使用MediaCodec实现
2.软编码,使用FFmpeg或者libx264库来实现。
本文分享在Android平台视频编码-软编码的实现,也就是用FFmpeg来实现视频的编码,rtmp推流到服务器上,相机采集视频将在下一篇文章分享。
流媒体服务器使用nginx-rtmp-module来进行搭建。
本文所使用FFmpeg的版本是4.1,关于FFmpeg编译成Android平台so库如果有需要,我将在下一篇文章分享说明。
视频编码比较耗cpu,上传视频数据的会耗网络io,所以需要开启新线程去处理,这里我用HandlerThread来处理视频的编码上传。
HandlerThread videoEncodeThread = new HandlerThread("video encode thread");
videoEncodeThread.start();
Handler mHandler = new Handler(videoEncodeThread.getLooper());
初始化编码相关操作
这里我们使用的是FFmpeg,所以在编码前我们会先做一些初始化以及参数设置工作。
FFmpeg初始化
av_register_all()
创建输出格式上下文
avformat_alloc_output_context2()
获取编码器
avcodec_find_encoder(AV_CODEC_ID_H264) 获取H264的编码器
设置编码器参数
- pix_fmt 像素的格式这里我们使用的AV_PIX_FMT_YUV420P,也就是YUV平面格式,三个平面分别存放Y、U、V数据。
- codec_type 编码器编码的数据类型
- framerate 帧率
- time_base 帧率的基本单位
- gop_size GOP的大小
使用给定的编码器和参数初始化编码上下文
avcodec_open2(pCodecCtx, pCodec, ¶m)
创建视频流
video_st = avformat_new_stream(ofmt_ctx, pCodec)
打开输出上下文
avio_open(&ofmt_ctx->pb, out_path, AVIO_FLAG_READ_WRITE)
写入输出头信息
avformat_write_header(ofmt_ctx, NULL)
/**
资源初始化
rtmp_width:视频的宽度
rtmp_height:视频的高度
*/
JNIEXPORT void JNICALL init(JNIEnv *env, jobject obj, jint rtmp_width, jint rtmp_height)
{
// 流媒体服务器地址
const char* out_file = "rtmp://10.1.2.23:1935/hls/test";
// 初始化
av_register_all();
// 初始化网络
avformat_network_init();
// 初始化Format上下文
avformat_alloc_output_context2(&av_format_context, NULL, "flv", out_file);
av_output_format = av_format_context->oformat;
// 打开连接服务器
if (avio_open(&av_format_context->pb, out_file, AVIO_FLAG_WRITE) < 0) {
LOGE("Failed to open output file! ");
return ;
}
// 获取视频编码器
video_codec = avcodec_find_encoder(AV_CODEC_ID_H264);
if(video_codec == NULL) {
LOGE("video codec is NULL");
return ;
}
video_stream = avformat_new_stream(av_format_context, video_codec);
video_stream->time_base.num = 1;
video_stream->time_base.den = 20; // 帧数
/* Some formats want stream headers to be separate. */
if (av_format_context->oformat->flags & AVFMT_GLOBALHEADER) {
video_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
if (video_stream == NULL) {
LOGE("video_stream is null");
return ;
}
// 视频编码器的参数设置
video_av_codec_context = video_stream->codec;
video_av_codec_context->codec_id = video_codec->id;
video_av_codec_context->codec_type = AVMEDIA_TYPE_VIDEO;
video_av_codec_context->pix_fmt = AV_PIX_FMT_YUV420P; // yuv420 yyyy uu vv 4个y对应一个uv
video_av_codec_context->width = rtmp_width;
video_av_codec_context->height = rtmp_height;
video_av_codec_context->bit_rate = 20 * 1000; // 码率 kbps
video_av_codec_context->gop_size = 60; // GOP图像组的宽度
video_av_codec_context->time_base.num = 1;
video_av_codec_context->time_base.den = 20;
//最大和最小量化系数
video_av_codec_context->qmin = 26;
video_av_codec_context->qmax = 52;
video_av_codec_context->max_b_frames = 3;
AVDictionary *param = 0;
if(video_av_codec_context->codec_id == AV_CODEC_ID_H264) {
/**
* ultrafast,superfast, veryfast, faster, fast, medium
* slow, slower, veryslow, placebo. 这是x264编码速度的选项
*/
av_dict_set(¶m, "preset", "slow", 0);
av_dict_set(¶m, "tune", "zerolatency", 0);
av_dict_set(¶m, "profile", "baseline", 0);
}
// 打开视频编码器
int ret = avcodec_open2(video_av_codec_context, video_codec, ¶m);
if (ret < 0) {
LOGE("Failed to open encoder! %d", ret);
return ;
}
// 分配YUV帧
video_frame_yuv = av_frame_alloc();
// 计算YUV帧所需的数据量
int picture_size = avpicture_get_size(video_av_codec_context->pix_fmt, video_av_codec_context->width, video_av_codec_context->height);
LOGE("picture_size:%d", picture_size);
picture_buf = (uint8_t *)av_malloc(picture_size * sizeof(uint8_t));
avpicture_fill((AVPicture *)video_frame_yuv, picture_buf, video_av_codec_context->pix_fmt, video_av_codec_context->width, video_av_codec_context->height);
// 写文件头
avformat_write_header(av_format_context, NULL);
av_new_packet(&enc_pkt, picture_size);
}
像素格式转换
AV_PIX_FMT_YUV420P,它是纯平面存储。总共三个平面,分别存放,Y、U、V数据。
当图像宽是width,高是height时,Y分量的大小就是width×heitht,而U是width×heitht/4,V也是U是width×heitht/4。
H264编码
首先我们需要了解两个数据结构AVFrame、AVPacket
AVFrame存放的是原始数据、AVPacket存放的是编码后的数据。
创建AVPacket
av_new_packet(&enc_pkt, picture_size);
开始编码
ret = avcodec_encode_video2(pCodecCtx, pFrameYUV);
输出一帧编码后的视频数据
ret = av_write_frame(pCodecCtx, &enc_pkt);
/**
yBuffer,uBuffer,vBuffer 由ByteBuffer.allocateDirect分配内存数据,存储yuv分量的数据,传到编码器进行编码,封装成rtmp协议的数据上传到流媒体服务器。
*/
JNIEXPORT void JNICALL sendFrameData(JNIEnv *env, jobject obj,
jobject yBuffer, jobject uBuffer, jobject vBuffer, jint row_stride)
{
if(video_stream == NULL) {
LOGE("init fail");
return ;
}
void* yByte = env->GetDirectBufferAddress(yBuffer);
void* uByte = env->GetDirectBufferAddress(uBuffer);
void* vByte = env->GetDirectBufferAddress(vBuffer);
int width = video_av_codec_context->width;
int height = video_av_codec_context->height;
memcpy(video_frame_yuv->data[0], yByte, width * height);
memcpy(video_frame_yuv->data[1], uByte, width * height / 4);
memcpy(video_frame_yuv->data[2], vByte, width * height / 4);
video_frame_yuv->pts = frameCount * (video_stream->time_base.den) / ((video_stream->time_base.num)*25);
int got_picture = 0;
// 视频编码
int ret = avcodec_encode_video2(video_av_codec_context, &enc_pkt, video_frame_yuv, &got_picture);
if(ret < 0) {
LOGE("Failed to encode! ret:%d", ret);
return ;
}
if (got_picture == 1)
{
enc_pkt.stream_index = video_stream->index;
LOGE("encode package pts:%lld, dts:%lld, duration:%d", enc_pkt.pts, enc_pkt.dts, enc_pkt.duration);
ret = av_write_frame(av_format_context, &enc_pkt);
LOGE("av_write_frame ret:%d", ret);
av_free_packet(&enc_pkt);
}
// 记录视频帧数
frameCount++;
}
释放资源
/**
结束rtmp推流,释放资源
*/
JNIEXPORT void JNICALL end(JNIEnv *env, jobject obj) {
LOGE("rtmp end, frameCount:%d", frameCount);
int ret = flush_encoder(av_format_context, video_stream->index);
if (ret < 0) {
LOGE("Flushing encoder failed\n");
return ;
}
// Write file trailer
av_write_trailer(av_format_context);
// Clean
if (video_stream != NULL) {
avcodec_close(video_stream->codec);
av_free(video_frame_yuv);
av_free(picture_buf);
}
avio_close(av_format_context->pb);
avformat_free_context(av_format_context);
}
小伙伴们有疑问的可以在下方评论区评论。