专栏首页Flutter入门Android PC投屏简单尝试(录屏直播)3—软解章(ImageReader+FFMpeg with X264)

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

使用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_64

CPU=arm
# PREFIX=$(pwd)/android/$CPU
PREFIX=../android-lib

cd x264

function 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

  make clean
  make -j8
  make install
}

build_one

cd ..

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_LDFLAGS


make 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-lib
        ffmpeg
        yuv
        # 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 tune
    AVDictionary *param = 0;
    //H.264
    if (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 muxing
    video_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 muxing
    if (avio_open(&ofmt_ctx->pb, out_path, AVIO_FLAG_READ_WRITE) < 0) {
        LOGE("Failed to open output file!\n");
        return -1;
    }

  //Write File Header
    avformat_write_header(ofmt_ctx, NULL);

       return 0;

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

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

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

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

            @Override
            public 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
    //初始化AVPacket
    av_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_64

CPU=arm
# PREFIX=$(pwd)/android/$CPU
PREFIX=../android-lib

cd x264

function 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 clean
  make -j8
  make install
}

build_one

cd ..

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_LDFLAGS


make 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

完美~~

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 偶遇FFmpeg(番外)——FFmpeg花样编译入魔2之单个SO库和ndk15之后单个SO文件

    因为最后会将文件合成libffmpeg.so文件,所以就不需要去改configure文件了!!

    deep_sadness
  • 偶遇FFmpeg(三)——Android集成

    其实这部分,不比多言了。虽然在网上可以找到很多类似的经验,但其实第一次使用还是要花费不少的时间。

    deep_sadness
  • Flv文件格式解析

    deep_sadness
  • Swift 有效的字母异位词 - LeetCode

    那么题目就变成了判读两个字符串的组成元素是否是一样的,我们用两个数组获取字符串的所有组成元素,然后排序后比较是否相等:

    韦弦zhy
  • 使用WiX制作具有时间限制的安装包

    WiX是Windows Installer XML的简称,它是用于制作Windows安装包的工具集。它支持命令行环境,开发者可以及将它集成到他们的编译过程中创建...

    葡萄城控件
  • 最新TensorFlow1.4.0教程完整版

    【导读】主题链路知识是我们专知的核心功能之一,为用户提供AI领域系统性的知识学习服务,一站式学习人工智能的知识,包含人工智能( 机器学习、自然语言处理、计算机视...

    WZEARW
  • 学界 | AAAI 18论文解读:基于强化学习的时间行为检测自适应模型

    AI 科技评论按:互联网上以视频形式呈现的内容在日益增多,对视频内容进行高效及时的审核也变得越来越迫切。因此,视频中的行为检测技术也是当下热点研究任务之一。本文...

    AI科技评论
  • 【干货】seq2seq模型实例:用Keras实现机器翻译

    【导读】近日,人工智能学者Ravindra Kompella发表一篇博客,介绍了作者实现的基于keras的机器翻译例子。作者通过一个seq2seq编码器-解码器...

    WZEARW
  • 2018年8月30日winPE系统和启动盘格式的区别和win安装系统启动方式的区别

    *********************************** winPE系统: Windows Preinstallation Environ...

    武军超
  • 建筑师自学编程,写出几十万行代码上线游戏首周卖出30万份!

    作为发布首周就登上steam畅销榜的国产武侠风格游戏《太吾绘卷》在最近喜迎更新,制作组在感谢了玩家支持之后也向玩家做出承诺,会在测试时不断改进并且优化游戏,甚至...

    一墨编程学习

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动