前言

音视频开发中,视频编码是另一个重要的部分,基于 FFmpeg 软件解码前面系列文章已经介绍过了。

接下来主要介绍软件编码这一块,包括视频编码、音频编码、为视频添加滤镜等,后续文章安排介绍 Android MediaCodec 硬件编解码。

前文我们对 x264、fdk-aac 及 FFmpeg 进行了整合编译,本文将利用编译好的 FFmpeg 库对 Android Camera2 采集的预览帧先进行渲染,然后利用 OpenGL 添加滤镜,最后读取渲染结果进行编码,生成 mp4 文件。

效果图

FFmpeg 视频编码流程

本文基于 Android Camera 2.0 API 采集的数据源进行编码,编码流程绘制是基于 FFmpeg 4.2.2 版本。

FFmpeg 视频编码流程图

相对于视频解码,编码流程多了一些写文件头尾的操作,需要停止编码时,通过刷入空帧来告诉编码器停止编码。

预览帧添加滤镜、编码流程

写 OpenGL ES 系列文章的时候,很多同学说为啥在 Native 层来写 demo ?

其实就是为了配合 FFmpeg 在视频解码和编码时添加滤镜,那么之前在 native 层写的所有关于滤镜的 demo ,现在可以直接拿过来用了,比如相机基础滤镜,相机抖音滤镜这些。

OpenGLCamera2

https://github.com/githubhaohao/OpenGLCamera2

这个项目有 30 多种滤镜供你参考。

我们首先通过 Android Camera2 预览回调获取预览帧(YUV):

private ImageReader.OnImageAvailableListener mOnPreviewImageAvailableListener = new ImageReader.OnImageAvailableListener() {@Overridepublic void onImageAvailable(ImageReader reader) {Image image = reader.acquireLatestImage();if (image != null) {if (mCamera2FrameCallback != null) {mCamera2FrameCallback.onPreviewFrame(CameraUtil.YUV_420_888_data(image), image.getWidth(), image.getHeight());}image.close();}}
};

之后利用 GLSurfaceView 来自动创建 OpenGL 环境,创建帧缓冲区对象(FBO),FBO 的主要好处就是保持图像的分辨率不变。

然后在 FBO 离屏渲染时添加滤镜,读取渲染结果作为 FFmpeg 视频编码的输入,最后绑定到 FBO 的纹理再去做屏幕渲染显示出来。

//离屏渲染,添加滤镜
glBindFramebuffer(GL_FRAMEBUFFER, m_DstFboId);
glViewport(0, 0, m_RenderImage.height, m_RenderImage.width); //相机的宽和高反了,
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram (m_ProgramObj);
glBindVertexArray(m_VaoId);UpdateMVPMatrix(0, 0, 1.0, 1.0);
GLUtils::setMat4(m_ProgramObj, "u_MVPMatrix", m_MVPMatrix);glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_SrcFboTextureId);
GLUtils::setInt(m_ProgramObj, "s_texture0", 0);
GLUtils::setInt(m_ProgramObj, "u_nImgType", IMAGE_FORMAT_RGBA);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);GetRenderFrameFromFBO();
glBindFramebuffer(GL_FRAMEBUFFER, 0);...//GetRenderFrameFromFBO 读取渲染结果,然后通过回调传入 FFmpeg 编码队列
void GLCameraRender::GetRenderFrameFromFBO() {LOGCATE("GLCameraRender::GetRenderFrameFromFBO m_RenderFrameCallback=%p", m_RenderFrameCallback);if(m_RenderFrameCallback != nullptr) {uint8_t *pBuffer = new uint8_t[m_RenderImage.width * m_RenderImage.height * 4];NativeImage nativeImage = m_RenderImage;nativeImage.format = IMAGE_FORMAT_RGBA;nativeImage.width = m_RenderImage.height;nativeImage.height = m_RenderImage.width;nativeImage.pLineSize[0] = nativeImage.width * 4;nativeImage.ppPlane[0] = pBuffer;glReadPixels(0, 0, nativeImage.width, nativeImage.height, GL_RGBA, GL_UNSIGNED_BYTE, pBuffer);m_RenderFrameCallback(m_CallbackContext, &nativeImage);delete []pBuffer;}
}

读取渲染结果的时候除了 glReadPixels , 之前提到高性能的读取方式还有 PBO 、HardwareBuffer等。

FFmpeg 视频编码实现

jni StartRecord 传入视频的宽、高、码率、帧率等参数,OnPreviewFrame 接口传入预览帧。

extern "C"
JNIEXPORT jint JNICALL
Java_com_byteflow_learnffmpeg_media_MediaRecorderContext_native_1StartRecord(JNIEnv *env,jobject thiz,jint recorder_type,jstring out_url,jint frame_width,jint frame_height,jlong video_bit_rate,jint fps) {//MediaRecorderContext 实际上只是对 SingleVideoRecorder 简单封装了一下 const char* url = env->GetStringUTFChars(out_url, nullptr);MediaRecorderContext *pContext = MediaRecorderContext::GetContext(env, thiz);env->ReleaseStringUTFChars(out_url, url);if(pContext) return pContext->StartRecord(recorder_type, url, frame_width, frame_height, video_bit_rate, fps);return 0;
}extern "C"
JNIEXPORT jint JNICALL
Java_com_byteflow_learnffmpeg_media_MediaRecorderContext_native_1StopRecord(JNIEnv *env,jobject thiz) {MediaRecorderContext *pContext = MediaRecorderContext::GetContext(env, thiz);if(pContext) return pContext->StopRecord();return 0;
}extern "C"
JNIEXPORT void JNICALL
Java_com_byteflow_learnffmpeg_media_MediaRecorderContext_native_1OnPreviewFrame(JNIEnv *env,jobject thiz,jint format,jbyteArray data,jint width,jint height) {int len = env->GetArrayLength (data);unsigned char* buf = new unsigned char[len];env->GetByteArrayRegion(data, 0, len, reinterpret_cast<jbyte*>(buf));MediaRecorderContext *pContext = MediaRecorderContext::GetContext(env, thiz);if(pContext) pContext->OnPreviewFrame(format, buf, width, height);delete[] buf;
}

视频编码器主要就是开启一个线程,然后不断地从预览帧队列中读取预览帧进行编码,视频编码器实现:

class SingleVideoRecorder {
public:SingleVideoRecorder(const char* outUrl, int frameWidth, int frameHeight, long bitRate, int fps);~SingleVideoRecorder();int StartRecord();int OnFrame2Encode(NativeImage *inputFrame);int StopRecord();private:static void StartH264EncoderThread(SingleVideoRecorder *context);int EncodeFrame(AVFrame *pFrame);
private:ThreadSafeQueue<NativeImage *> m_frameQueue;char m_outUrl[1024] = {0};int m_frameWidth;int m_frameHeight;int m_frameIndex = 0;long m_bitRate;int m_frameRate;AVPacket m_avPacket;AVFrame  *m_pFrame = nullptr;uint8_t *m_pFrameBuffer = nullptr;AVCodec  *m_pCodec = nullptr;AVStream *m_pStream = nullptr;AVCodecContext *m_pCodecCtx = nullptr;AVFormatContext *m_pFormatCtx = nullptr;thread *m_encodeThread = nullptr;SwsContext *m_SwsContext = nullptr;volatile int m_exit = 0;
};

视频编码循环:

void SingleVideoRecorder::StartH264EncoderThread(SingleVideoRecorder *recorder) {LOGCATE("SingleVideoRecorder::StartH264EncoderThread start");//停止编码且队列为空时退出编码循环while (!recorder->m_exit || !recorder->m_frameQueue.Empty()){if(recorder->m_frameQueue.Empty()) {//队列为空,休眠等待usleep(10 * 1000);continue;}//从队列中取一帧预览帧NativeImage *pImage = recorder->m_frameQueue.Pop();AVFrame *pFrame = recorder->m_pFrame;AVPixelFormat srcPixFmt = AV_PIX_FMT_YUV420P;switch (pImage->format) {case IMAGE_FORMAT_RGBA:srcPixFmt = AV_PIX_FMT_RGBA;break;case IMAGE_FORMAT_NV21:srcPixFmt = AV_PIX_FMT_NV21;break;case IMAGE_FORMAT_NV12:srcPixFmt = AV_PIX_FMT_NV12;break;case IMAGE_FORMAT_I420:srcPixFmt = AV_PIX_FMT_YUV420P;break;default:LOGCATE("SingleVideoRecorder::StartH264EncoderThread unsupport format pImage->format=%d", pImage->format);break;}if(srcPixFmt != AV_PIX_FMT_YUV420P) {if(recorder->m_SwsContext == nullptr) {recorder->m_SwsContext = sws_getContext(pImage->width, pImage->height, srcPixFmt,recorder->m_frameWidth, recorder->m_frameHeight, AV_PIX_FMT_YUV420P,SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);}//格式不同时,需要转换为编码器的目标格式 AV_PIX_FMT_YUV420P,转换之后的图像在 pFrame if(recorder->m_SwsContext != nullptr) {int slice = sws_scale(recorder->m_SwsContext, pImage->ppPlane, pImage->pLineSize, 0,recorder->m_frameHeight, pFrame->data, pFrame->linesize);LOGCATE("SingleVideoRecorder::StartH264EncoderThread sws_scale slice=%d", slice);}}//设置 ptspFrame->pts = recorder->m_frameIndex++;//编码一帧recorder->EncodeFrame(pFrame);//释放预览帧内存NativeImageUtil::FreeNativeImage(pImage);delete pImage;}LOGCATE("SingleVideoRecorder::StartH264EncoderThread end");
}

编码一帧的函数:

int SingleVideoRecorder::EncodeFrame(AVFrame *pFrame) {int result = 0;result = avcodec_send_frame(m_pCodecCtx, pFrame);if(result < 0){LOGCATE("SingleVideoRecorder::EncodeFrame avcodec_send_frame fail. ret=%d", result);return result;}while(!result) {result = avcodec_receive_packet(m_pCodecCtx, &m_avPacket);if (result == AVERROR(EAGAIN) || result == AVERROR_EOF) {return 0;} else if (result < 0) {LOGCATE("SingleVideoRecorder::EncodeFrame avcodec_receive_packet fail. ret=%d", result);return result;}LOGCATE("SingleVideoRecorder::EncodeFrame frame pts=%ld, size=%d", m_avPacket.pts, m_avPacket.size);m_avPacket.stream_index = m_pStream->index;av_packet_rescale_ts(&m_avPacket, m_pCodecCtx->time_base, m_pStream->time_base);m_avPacket.pos = -1;av_interleaved_write_frame(m_pFormatCtx, &m_avPacket);av_packet_unref(&m_avPacket);}return 0;
}

源码地址

https://github.com/githubhaohao/LearnFFmpeg

FFmpeg 视频录制 - 视频添加滤镜和编码相关推荐

  1. FFmpeg 开发(10):FFmpeg 视频录制 - 视频添加滤镜和编码

    音视频开发中,视频编码是另一个重要的部分,基于 FFmpeg 软件解码前面系列文章已经介绍过了,接下来主要介绍软件编码这一块,包括视频编码.音频编码.为视频添加滤镜等.后期文章安排将介绍 Androi ...

  2. android javacv 录像,使用JavaCV(ffmpeg)录制视频

    使用JavaCV(ffmpeg)录制视频 JavaCV是对各种常用计算机视觉库的封装后的一组jar包,其中封装了ffmpeg.OpenCV.libdc1394.OpenKinect.videoInpu ...

  3. Android使用GpuImage给图片、视频、相机添加滤镜

    网上关于GpuImage使用的资料较少,我在使用的过程中整理出一个Demo,分享出来,希望帮助到有需要的人 Demo比较简单 首先是给图片加滤镜,这个比较简单,网上也有现成的例子 给视频添加滤镜,这里 ...

  4. Android视频 录制视频被拉伸解决方案

    场景: 用camera2,api 和ffmpeg录制视频,视频画面被拉伸 问题: 如何解决录制视频画面被拉伸的问题? 解决方法: 1,通过Render的View来进行等比显示. 2,设置合适的宽高参数 ...

  5. Android实现本地视频+录制视频+视频压缩上传

    今天研究了一下视频上传的处理,还包括研究可以视频压缩,本地视频.录制视频上传.本地视频获取需要适配手机,这个建议自己获取本地视频列表自己实现.不然就会出现路径找不到返回 null 好东西都是要分享给大 ...

  6. 视频添加滤镜怎么弄?这几个方法建议收藏

    上学的时候,由于上交的视频作业得到了领导的认可,所以学校再次给我们安排了一个拍摄"校园风光"的任务.在我们拍摄完视频素材后,对视频进行编辑时,总觉得画面有点单调,于是我们就使用软件 ...

  7. webrtc 入门第二章 音视频录制

    webrtc 入门第二章 音视频录制 一.介绍 1.媒体录制原理 ​ 在很多场景中回放音视频资源的需求是非常重要的例如会议,直播授课等.任何媒体形式的表情都可进行录制,如 ,,等.其中内容更加自由用户 ...

  8. Android (系统+自定义)短视频录制(含暂停继续录制功能) 总结

    前言 在Android开发中自然少不了对视频录制的需求,然而视频录制虽然有系统提供给我们能够直接使用的API,但是我们往往在完成需求的过程中需要自定义实现短视频录制.网上虽然也有不少资料,但是总是零零 ...

  9. 《微课实战:Camtasia Studio入门精要》——第2章 录制视频 2.1 录制视频基本常识...

    本节书摘来自异步社区<微课实战:Camtasia Studio入门精要>一书中的第2章,第2.1节,作者 于化龙,沈婷婷,郝雨,更多章节内容可以访问云栖社区"异步社区" ...

最新文章

  1. 深度学习——数据预处理篇
  2. php 获取对象中的元素个数组长度,php数组长度怎么获取
  3. Bitbucket Cloud的新IP地址
  4. ES5新增对象的属性和方法
  5. rest端点_REST:使用Controller端点?
  6. 信息学奥赛一本通(1164:digit函数)
  7. 有关国土的几个重大项目
  8. jsp简单案例(供小白学习)
  9. 如何将google切片发布成arcgis服务并生成tpk包
  10. 校对双层PDF中的隐藏文本
  11. 面向对象编程实例——句柄类的使用
  12. BOS v2.0后台管理系统界面通用解决方案
  13. 输入法突然变成繁体字的解决方法
  14. [python][project][爬虫] 堆糖网图片下载
  15. tokenize java,Java split string - Java tokenize string examples - 入门小站-rumenz.com
  16. pmos低电平驱动_驱动篇 -PMOS管应用
  17. NYOJ-20 吝啬的国度 AC
  18. awb入门(4).颜色恒常方法(一)gamut mapping
  19. 使用numpy计算准确率
  20. 新绝代双骄3终极全攻略6

热门文章

  1. matlab模拟晶粒生长,matlab模拟晶粒长大?
  2. linux 网关 优化,基于Linux的家庭网关优化.pdf
  3. 网络设备自动化运维工具——Nornir3.0.0入门笔记
  4. LINUX设置临时路径
  5. 最实用的vue刷新当前页面,provide / inject 组合 方式实现vue页面刷新
  6. ARFoundation系列讲解 - 72 AR测距二
  7. python的ppt库_Python绘图库matplotlib快速入门.ppt
  8. 我国光伏产业最新发展情况分析
  9. 2022年高压电工题库及模拟考试
  10. html5添加下划线虚线,web中添加下划线的方法及优缺点