使用FFmpeg进行软件解码并通过RTMP进行推流

  1. 编译带有x264的FFmpeg
  2. 编写FFmpeg代码进行推流

通过ImageReader的回调,我们就可以得到截屏的数据了。第一遍文章是通过自定义的Socket 协议进行传输。这里通过FFmpeg,将得到的数据进行软件编码,然后同样通过RTMP进行推流。

配套使用示意图.png

编译

去官网下载源码,并且解压。按照下面的文件夹路径进行存放。

├── ffmpeg├── x264└── others....

编写编译脚本。
其实我们是先编译出libx264.a 然后与ffmpeg进行交叉编译。编译出完整的libFFmpeg.so 文件。

脚本放到ffmpeg的目录下进行运行就可以了。
这里需要修改的就是你自己的ndk路径了

#!/bin/bash
NDK=/Users/Cry/Library/Android/sdk/android-ndk-r14b
PLATFORM=$NDK/platforms/android-19/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64CPU=arm
# PREFIX=$(pwd)/android/$CPU
PREFIX=../android-libcd x264function build_one
{./configure \--prefix=$PREFIX \--enable-static \--enable-shared \--enable-pic \--disable-asm \--disable-cli \--host=arm-linux \--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \--sysroot=$PLATFORMmake cleanmake -j8make install
}build_onecd ..OUT_PREFIX=$(pwd)/android/$CPU
# 加入x264编译库
EXTRA_CFLAGS="-I./android-lib/include"
EXTRA_LDFLAGS="-L./android-lib/lib"function build_two
{
./configure \--target-os=linux \--prefix=$OUT_PREFIX \--enable-cross-compile \--enable-runtime-cpudetect \--disable-asm \--disable-doc \--arch=arm \--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \--disable-stripping \--nm=$TOOLCHAIN/bin/arm-linux-androideabi-nm \--sysroot=$PLATFORM \--enable-gpl \--enable-static \--disable-shared \--enable-version3 \--enable-small \--enable-libx264 \--enable-encoder=libx264 \--enable-zlib \--disable-ffprobe \--disable-ffplay \--disable-ffmpeg \--disable-ffserver \--extra-cflags=$EXTRA_CFLAGS \--extra-ldflags=$EXTRA_LDFLAGSmake clean
make -j8
make install# 这段解释见后文
$TOOLCHAIN/bin/arm-linux-androideabi-ld -rpath-link=$PLATFORM/usr/lib -L$PLATFORM/usr/lib -L$OUT_PREFIX/lib -soname libffmpeg.so -shared -nostdlib -Bsymbolic --whole-archive --no-undefined -o $OUT_PREFIX/libffmpeg.so \android-lib/lib/libx264.a \libavcodec/libavcodec.a \libavfilter/libavfilter.a \libswresample/libswresample.a \libavformat/libavformat.a \libavutil/libavutil.a \libswscale/libswscale.a \libpostproc/libpostproc.a \libavdevice/libavdevice.a \-lc -lm -lz -ldl -llog --dynamic-linker=/system/bin/linker $TOOLCHAIN/lib/gcc/arm-linux-androideabi/4.9.x/libgcc.a
}
build_two

编译结果

image.png

image.png

这个就是我们想要的带有x264的ffmpeg了

因为我们这里得到的数据将是RGBA的数据,所以我们还需要将其转成YUV420P,进行处理。我们需要libyuv,使用这个库进行转换能大大提升我们的效果。而且使用起来非常方便。
所以我们也将其加入编译

  1. 下载源码
  2. 配置项目
    将源码全部复制到

    image.png

同时我们注意到,这里面就已经配置好Cmake文件了。我只需要将其做一下简单的修改,就可以使用了

image.png

将我们不需要的so文件和bin文件的安装给去掉。

接下来配置我们自己的cmake文件


#libyuv
include_directories(${CMAKE_SOURCE_DIR}/libs/libyuv/include)
# 这样就可以直接使用内部的cmake文件了
add_subdirectory(${CMAKE_SOURCE_DIR}/libs/libyuv)
#...部分省略
#同时将其链接到我们自己的库中,来进行使用
target_link_libraries( # Specifies the target library.native-libffmpegyuv# Links the target library to the log library# included in the NDK.${log-lib})

进行代码的编写

  1. RTMP的链接
    同样,需要先进行RTMP的链接。FFMpeg不同的是,因为自己就有编码器,所以可以直接将头写到流里。完成publish

  2. 使用FFmpeg的必备套路。
    注册编码器和网络。(因为真的有用到啊)

    av_register_all();
  1. 同样的套路。在使用编码器之前,都需要配置编码器的参数。
    在FFmpeg中,同样需要MediaFormat和Encoder。而且ffmpeg 的编程离不开各种上下文对象.所以这里就是先去获取上下文对象。然后给其配置参数。进行初始化
 //AVFormat的上下文对象,里面配置format的信息
AVFormatContext *ofmt_ctx;//通过我们给的地址,和''flv'的 格式名称来分配上下文avformat_alloc_output_context2(&ofmt_ctx, NULL, "flv", out_path);

这个上下文十分重要和常见。他是包含IO的格式上下文。我们先获取他。
接着。我们需要来找到我们的编码器

AVCodec *pCodec;
//这里直接通过ID进行查找
pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);if (!pCodec) {LOGI("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;

这里主要配置的都是一些常见的参数。包括编码器的ID,视频的长宽信息,比特率,帧率,时基和gop_size

接着配置一些 H.264需要的参数

    //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 tuneAVDictionary *param = 0;//H.264if (pCodecCtx->codec_id == AV_CODEC_ID_H264) {
//        av_dict_set(&param, "preset", "slow", 0);/*** 这个非常重要,如果不设置延时非常的大* ultrafast,superfast, veryfast, faster, fast, medium* slow, slower, veryslow, placebo. 这是x264编码速度的选项*/av_dict_set(&param, "preset", "superfast", 0);av_dict_set(&param, "tune", "zerolatency", 0);}

这里有两个必须要注意的地方。

  1. pCodecCtx->qcompress = 0.6;
    //最大和最小量化系数
    pCodecCtx->qmin = 10;
    pCodecCtx->qmax = 51;
    这几个参数必须配置对。如果不是这样的话,好像是会出错的。

  2. 编码速度的选项。这个也很有影响

接着配置完参数,我们就开启encoder

   if (avcodec_open2(pCodecCtx, pCodec, &param) < 0) {LOGE("Failed to open encoder!\n");return -1;}

因为我们这儿只推流视频,所以,我们还需要创建一个stream.将我们的编码器信息同样保存到这个视频流中

//Add a new stream to output,should be called by the user before avformat_write_header() for muxingvideo_st = avformat_new_stream(ofmt_ctx, pCodec);if (video_st == NULL) {return -1;}video_st->time_base.num = 1;video_st->time_base.den = fps;
//    video_st->codec = pCodecCtx;video_st->codecpar->codec_tag = 0;avcodec_parameters_from_context(video_st->codecpar, pCodecCtx);

最后,就是通过avio_open 打开链接,进行链接。
并且我们知道进行推流,必须先将其头部的编码器信息写入,才可以。所以同样
avformat_write_header 写入信息,这样,publish RTMP成功了。

//Open output URL,set before avformat_write_header() for muxingif (avio_open(&ofmt_ctx->pb, out_path, AVIO_FLAG_READ_WRITE) < 0) {LOGE("Failed to open output file!\n");return -1;}//Write File Headeravformat_write_header(ofmt_ctx, NULL);return 0;

接下来,就是推送实际的nal了

  1. RTMP数据的发送
    回顾ImageReader的配置
        imageReader = ImageReader.newInstance(width, height, PixelFormat.RGBA_8888, 5);

我们要求输出的是RGBA格式的Image数据。

通过ImageReader的回调,我们可以得到Image数据

            @Overridepublic void onImageAvailable(ImageReader reader) {Image image = reader.acquireLatestImage();if (image != null) {long timestamp = image.getTimestamp();if (this.timestamp == 0) {this.timestamp = timestamp;if (VERBOSE) {Log.d(TAG, "onImageAvailable timeStamp=" + this.timestamp);}} else {if (VERBOSE) {long delta = timestamp - this.timestamp;Log.d(TAG, "onImageAvailable timeStamp delta in ms=" + delta / 1000000);}}Image.Plane[] planes = image.getPlanes();//因为我们要求的是RGBA格式的数据,所以全部的存储在planes[0]中Image.Plane plane = planes[0];//由于Image中的缓冲区存在数据对齐,所以其大小不一定是我们生成ImageReader实例时指定的大小,//ImageReader会自动为画面每一行最右侧添加一个padding,以进行对齐,对齐多少字节可能因硬件而异,//所以我们在取出数据时需要忽略这一部分数据。int rowStride = plane.getRowStride();int pixelStride = plane.getPixelStride();int rowPadding = rowStride - pixelStride * width;ByteBuffer buffer = plane.getBuffer();//将得到的buffer 和 宽高传入进行处理FFmpegSender.getInstance().rtmpSend(buffer, height, width * 4, rowPadding);image.close();}}

发送的方法。
1.我们这里传入了未编码的RGBA数据,需要先转成YUV420P.
AVFrame来保存未编码的数据。所以我们需要先给其分配内存空间和数据

pFrameYUV = av_frame_alloc();int picture_size = av_image_get_buffer_size(pCodecCtx->pix_fmt, pCodecCtx->width,pCodecCtx->height, 1);uint8_t *buffers = (uint8_t *) av_malloc(picture_size);//将buffers的地址赋给AVFrame中的图像数据,根据像素格式判断有几个数据指针av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, buffers, pCodecCtx->pix_fmt,pCodecCtx->width, pCodecCtx->height, 1);

然后,我们将我们的数据转成yuv,并且将数据传递给pFrameYUV

    //之前我们说过,得到的数据是有字节对齐的问题的。我们在这里,进行处理。得到真正的argb数据jbyte *srcBuffer = static_cast<jbyte *>(env->GetDirectBufferAddress(buffer));jbyte *dest = new jbyte[yuv_width * yuv_height * 4];int offset = 0;for (int i = 0; i < row;i++) {memcpy(dest + offset, srcBuffer + offset + i * rowPadding, stride);offset += stride;}

利用 libyuv 将数据转成yuv420p,同时保存起来

    libyuv::ConvertToI420((uint8_t *) dest, yuv_width * yuv_height,pFrameYUV->data[0], yuv_width,pFrameYUV->data[1], yuv_width / 2,pFrameYUV->data[2], yuv_width / 2,0, 0,yuv_width, yuv_height,yuv_width, yuv_height,libyuv::kRotate0, libyuv::FOURCC_ABGR);
  1. 我们需要将数据送入编码器进行编码
    先配置参数
    pFrameYUV->pts = count;pFrameYUV->format = AV_PIX_FMT_YUV420P;pFrameYUV->width = yuv_width;pFrameYUV->height = yuv_height;

AVPacket是存储编码之后的数据的。我们需要进行的操作就是将AVFrame送入编码器,然后得到AVPacket. 所以我们对其进行初始化。并且按照上面所说。来得到包含编码数据的AvPacket

   //例如对于H.264来说。1个AVPacket的data通常对应一个NAL//初始化AVPacketav_init_packet(&enc_pkt);//开始编码YUV数据ret = avcodec_send_frame(pCodecCtx, pFrameYUV);if (ret != 0) {LOGE("avcodec_send_frame error");return -1;}//获取编码后的数据ret = avcodec_receive_packet(pCodecCtx, &enc_pkt);//是否编码前的YUV数据av_frame_free(&pFrameYUV);if (ret != 0 || enc_pkt.size <= 0) {LOGE("avcodec_receive_packet error");avError(ret);return -2;}

得到编码后的数据,再对其进行参数配置,需要注意的pts 和dts的配置,这里的方式不对。这里把他当作是恒定的帧率来处理来。但实际上,因为由当前的实际来决定。

    enc_pkt.stream_index = video_st->index;AVRational time_base = ofmt_ctx->streams[0]->time_base;//{ 1, 1000 };enc_pkt.pts = count * (video_st->time_base.den) / ((video_st->time_base.num) * fps);enc_pkt.dts = enc_pkt.pts;enc_pkt.duration = (video_st->time_base.den) / ((video_st->time_base.num) * fps);LOGI("index:%d,pts:%lld,dts:%lld,duration:%lld,time_base:%d,%d",count,(long long) enc_pkt.pts,(long long) enc_pkt.dts,(long long) enc_pkt.duration,time_base.num, time_base.den);enc_pkt.pos = -1;
  1. 我们需要通过RTMP协议进行发送数据
    这部分很简单,只要调用write方法就可以完成了。
    ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);if (ret != 0) {avError(ret);LOGE("av_interleaved_write_frame failed");}count++;return 0;

最后是关闭的方法。
关闭的时候,我们需要释放掉我们创建的IO链接/AVFormatContext和Encoder。

    if (video_st)//encode包含在流中了avcodec_close(video_st->codec);if (ofmt_ctx) {//网络的指针保留在AVFormatContext中avio_close(ofmt_ctx->pb);//同时自己也要释放avformat_free_context(ofmt_ctx);ofmt_ctx = NULL;}return 0;

总结

需要注意的两点

1. FFmpeg的裁剪编译

直接编译出来的so文件巨大。在APK文件中6M大小。

  • 定位裁剪需求
    我们根据之前的文章,来分析和定位裁剪的脚本。
    整个流程中,我们只需要libx264 的编码器。flv的muxer 和 RTMP协议。因为RTMP协议是基于TCP的。所以我们也打开tcp协议。

  • 编写脚本
    基于上面的分析,我们修改了FFmpeg的配置

#!/bin/bash
NDK=/Users/Cry/Library/Android/sdk/android-ndk-r14b
PLATFORM=$NDK/platforms/android-19/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64CPU=arm
# PREFIX=$(pwd)/android/$CPU
PREFIX=../android-libcd x264function build_one
{./configure \--prefix=$PREFIX \--enable-static \--enable-shared \--enable-pic \--disable-asm \--disable-cli \--host=arm-linux \--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \--sysroot=$PLATFORM \--extra-cflags="-fPIC -marm -DX264_VERSION -DANDROID -DHAVE_PTHREAD -DNDEBUG -static -D__ARM_ARCH_7__ -D__ARM_ARCH_7A__ -O3 -march=armv7-a -mfpu=neon -mtune=generic-armv7-a -mfloat-abi=softfp -ftree-vectorize -mvectorize-with-neon-quad -ffast-math" \make cleanmake -j8make install
}build_onecd ..OUT_PREFIX=$(pwd)/android/$CPU
# 加入x264编译库
EXTRA_CFLAGS="-I./android-lib/include"
EXTRA_LDFLAGS="-L./android-lib/lib"function build_two
{
./configure \--target-os=linux \--prefix=$OUT_PREFIX \--enable-cross-compile \--enable-runtime-cpudetect \--disable-asm \--disable-doc \--arch=arm \--cc=$TOOLCHAIN/bin/arm-linux-androideabi-gcc \--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \--disable-stripping \--nm=$TOOLCHAIN/bin/arm-linux-androideabi-nm \--sysroot=$PLATFORM \--enable-gpl \--enable-static \--disable-shared \--enable-version3 \--enable-small \--enable-libx264 \--disable-encoders \--enable-encoder=libx264 \--disable-muxers \--enable-muxer=flv \--enable-muxer=h264 \--disable-decoders \--disable-demuxers \--disable-parsers \--enable-parser=aac \--enable-parser=h264 \--disable-protocols \--enable-protocol=file \--enable-protocol=ffrtmphttp \--enable-protocol=rtmp \--enable-protocol=tcp \--disable-filters \--disable-bsfs \--disable-indevs \--disable-outdevs \--disable-ffprobe \--disable-ffplay \--disable-ffmpeg \--disable-ffserver \--extra-cflags=$EXTRA_CFLAGS \--extra-ldflags=$EXTRA_LDFLAGSmake clean
make -j8
make install$TOOLCHAIN/bin/arm-linux-androideabi-ld -rpath-link=$PLATFORM/usr/lib -L$PLATFORM/usr/lib -L$OUT_PREFIX/lib -soname libffmpeg.so -shared -nostdlib -Bsymbolic --whole-archive --no-undefined -o $OUT_PREFIX/libffmpeg.so \android-lib/lib/libx264.a \libavcodec/libavcodec.a \libavfilter/libavfilter.a \libswresample/libswresample.a \libavformat/libavformat.a \libavutil/libavutil.a \libswscale/libswscale.a \libpostproc/libpostproc.a \libavdevice/libavdevice.a \-lc -lm -lz -ldl -llog --dynamic-linker=/system/bin/linker $TOOLCHAIN/lib/gcc/arm-linux-androideabi/4.9.x/libgcc.a
}
build_two

image.png

  • 结果
    原大小

    image.png

现在的大小

image.png

在APK中的大小

image.png

完美~~

作者:deep_sadness
链接:https://www.jianshu.com/p/6559567a973c
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

Android PC投屏简单尝试(录屏直播)3—软解章(ImageReader+FFMpeg with X264)相关推荐

  1. Android PC投屏简单尝试(录屏直播)2—硬解章(MediaCodec+RMTP)

    代码地址 :https://github.com/deepsadness/MediaProjectionDemo 想法来源 上一边文章的最后说使用录制的Api进行录屏直播.本来这边文章是预计在5月份完 ...

  2. Android PC投屏简单尝试—最终章2

    源码地址:https://github.com/deepsadness/AppRemote 上一章中,我们简单实现了PC的投屏功能. 但是还是存在这一些缺陷. 屏幕的尺寸数据是写死的 不能通过PC来对 ...

  3. Android PC投屏简单尝试—最终章1

    回顾之前的几遍文章,我们分别通过RMTP协议和简单的Socket 发送Bitmap图片的Base64编码来完成投屏. 回想这系列文章的想法来源-Vysor,它通过 USB来进行连接的.又看到了 scr ...

  4. Android PC投屏简单尝试- 自定义协议章(Socket+Bitmap)

    代码地址 :https://github.com/deepsadness/MediaProjectionDemo 效果预览 投屏效果预览 简单说明: 使用Android MediaProjection ...

  5. java无线投屏代码,Android PC投屏功能实现的示例代码

    本文介绍了Android PC投屏功能实现的示例代码,分享给大家,具体如下: 效果预览 投屏效果预览 简单说明: 使用Android MediaProjection Api来完成视频的截图 通过Web ...

  6. mac android 录屏软件,mac录屏怎么录内置声音?详细的解决方案

    原标题:mac录屏怎么录内置声音?详细的解决方案 mac录屏怎么录内置声音?大家使用Mac电脑最大的问题就是对操作系统的不熟悉,就像是刚接触Windows系统一样,开始都会有一定的不熟悉,其实并没有那 ...

  7. Android 如何实现App在后台录屏

    在 Android 中实现 App 在后台录屏主要需要使用到 MediaProjection API. MediaProjection API 是 Android 5.0(API Level 21)引 ...

  8. 录屏工具下载哪个好?分享:超简单的录屏工具及实用方法

    在短视频盛行,知识付费,粉丝经济的背景下,下载一款好用的录屏工具可以如虎添翼,为自己制作视频助力.然而面对各种各样的录屏工具,很多人不知道下载哪个好.一旦录屏工具没有选对,即使下载了,不会用也没有意义 ...

  9. 电脑有什么超简单的录屏方法

    录屏是我们经常需要使用的一项功能,授课.演示.讲解.会议记录等等经常需要使用这一功能,但是,相比于手机端的录屏功能来说,电脑端的录屏功能显得还没有那么完善. 一番探索之后,我发现了解决电脑录屏问题的一 ...

最新文章

  1. 损失函数的意义和作用_哈佛CASTER | 基于化学子结构表征预测药物相互作用
  2. uboot流程——命令行模式以及命令处理介绍
  3. 【经验】新人学习写程序的第一道坎
  4. 接口继承中一个常见问题的思考
  5. GraphQL的query:一个最简单的例子
  6. Android之替换App桌面图标
  7. 深度学习pytorch--线性回归(二)
  8. 设计模式之行为类模式PK
  9. 字体大小自适应屏幕分辨率 CSS解决方案
  10. 联想服务器虚拟化解决方案,联想虚拟化解决方案
  11. html5中web存储(localStorage、sessionStorage)与cookie的区别?????
  12. 死锁的产生原因和解决办法
  13. 微信公众号网页授权步骤过程
  14. c语言中windows.h是什么意思,c语言中memory.h有什么作用
  15. 跟Kaggle做泰坦尼克乘客生存分析
  16. android课程设计健身,健身软件课程设计.doc
  17. 三,java流程控制常见练习题及面试题
  18. 解决imageview 不显示图片问题
  19. java yyyy m d_JAVA SimpleDateFormat使用YYYY-MM-dd的坑
  20. 计算机组成原理——磁盘存储器的技术指标

热门文章

  1. 使用PHP自带的过滤验证函数:Filter
  2. 全新的互动广告牌,待遇男女有别
  3. 统计自然语言处理笔记
  4. 吴恩达 coursera ML 第十六课总结+作业答案
  5. 信号处理:希尔伯特黄变换
  6. Ubuntu下搭建MPI并行计算环境
  7. 科大星云诗社动态20210411
  8. 科大星云诗社动态20210510
  9. 宅福利-宅家抗疫,你我同在2020-01-30
  10. java获取异常的数据_Java(8题):异常,通过try catch进行处理,登录,商品,使用jdbc进行读取,详细图析...