我的编程空间,编程开发者的网络收藏夹
学习永远不晚

【Android FFMPEG 开发】FFMPEG 直播功能完整流程 + 源码 ( 源码交叉编译 -> AS工程配置 -> 音视频打开/读取/解码/格式转换 -&

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

【Android FFMPEG 开发】FFMPEG 直播功能完整流程 + 源码 ( 源码交叉编译 -> AS工程配置 -> 音视频打开/读取/解码/格式转换  -&

文章目录I . FFMPEG 播放视频流程总结II . FFMPEG 下载及交叉编译III . Android Studio 配置 FFMPEG 函数库IV . FFMPEG 初始化V . FFMPEG 获取 AVStream 音视频流VI . FFMPEG 获取编解码器VII . FFMPEG 读取音视频流中的数据到 AVPacket ( 压缩编码后的数据包 )VIII . FFMPEG AVFrame 图像格式转换 YUV -> RGBAIX . ANativeWindow 原生绘制X . FFMPEG 音频重采样XI . OpenSLES 播放音频XII . FFMPEG 音视频同步XIII . GitHub 代码地址


本博客属于总结性质的博客 , 开发时可以参考该博客的代码示例 , 可以直接使用 ; 知识点比较概括 , 只描述操作流程及核心源码 , 没有具体原理说明 , 详细的原理知识去具体的单条知识点博客中查看 ;


代码及播放效果 :

1 . GitHub 代码地址 : FFMPEG 直播示例

2 . 效果展示 : 使用的是湖南卫视的直播源 rtmp://58.200.131.2:1935/livetv/hunantv
在这里插入图片描述



I . FFMPEG 播放视频流程总结

FFMPEG 播放视频流程 : 视频中包含图像和音频 ;


1 . FFMPEG 交叉编译配置 : 【Android FFMPEG 开发】FFMPEG 交叉编译配置 ( 下载 | 配置脚本 | 输出路径 | 函数库配置 | 程序配置 | 组件配置 | 编码解码配置 | 交叉编译配置 | 最终脚本 )


2 . Android Studio 工程配置 FFMPEG : 【Android FFMPEG 开发】Android Studio 工程配置 FFMPEG ( 动态库打包 | 头文件与函数库拷贝 | CMake 脚本配置 )


3 . FFMPEG 初始化 : 【Android FFMPEG 开发】FFMPEG 初始化 ( 网络初始化 | 打开音视频 | 查找音视频流 )


4 . FFMPEG 获取 AVStream 音视频流 : 【Android FFMPEG 开发】FFMPEG 获取 AVStream 音视频流 ( AVFormatContext 结构体 | 获取音视频流信息 | 获取音视频流个数 | 获取音视频流 )


5 . FFMPEG 获取 AVCodec 编解码器 : 【Android FFMPEG 开发】FFMPEG 获取编解码器 ( 获取编解码参数 | 查找编解码器 | 获取编解码器上下文 | 设置上下文参数 | 打开编解码器 )


6 . FFMPEG 读取音视频流中的数据到 AVPacket : 【Android FFMPEG 开发】FFMPEG 读取音视频流中的数据到 AVPacket ( 初始化 AVPacket 数据 | 读取 AVPacket )


7 . FFMPEG 解码 AVPacket 数据到 AVFrame ( 音频 / 视频数据解码 ) : 【Android FFMPEG 开发】FFMPEG 解码 AVPacket 数据到 AVFrame ( AVPacket->解码器 | 初始化 AVFrame | 解码为 AVFrame 数据 )


8 . FFMPEG AVFrame 图像格式转换 YUV -> RGBA : 【Android FFMPEG 开发】FFMPEG AVFrame 图像格式转换 YUV -> RGBA ( 获取 SwsContext | 初始化图像数据存储内存 | 图像格式转换 )


9 . FFMPEG ANativeWindow 原生绘制 准备 : 【Android FFMPEG 开发】FFMPEG ANativeWindow 原生绘制 ( Java 层获取 Surface | 传递画布到本地 | 创建 ANativeWindow )


10 . FFMPEG ANativeWindow 原生绘制 : 【Android FFMPEG 开发】FFMPEG ANativeWindow 原生绘制 ( 设置 ANativeWindow 缓冲区属性 | 获取绘制缓冲区 | 填充数据到缓冲区 | 启动绘制 )


11 . FFMPEG 音频重采样 : 【Android FFMPEG 开发】FFMPEG 音频重采样 ( 初始化音频重采样上下文 SwrContext | 计算音频延迟 | 计算输出样本个数 | 音频重采样 swr_convert )


12 . FFMPEG 音频播放 : 【Android FFMPEG 开发】OpenSLES 播放音频 ( 创建引擎 | 输出混音设置 | 配置输入输出 | 创建播放器 | 获取播放/队列接口 | 回调函数 | 开始播放 | 激活回调 )


13 . FFMPEG 音视频同步 : 【Android FFMPEG 开发】FFMPEG 音视频同步 ( 音视频同步方案 | 视频帧 FPS 控制 | H.264 编码 I / P / B 帧 | PTS | 音视频同步 )


14 . FFMPEG 直播示例 : 【Android FFMPEG 开发】FFMPEG 直播功能完整流程 + 源码 ( 源码交叉编译 -> AS工程配置 -> 音视频打开/读取/解码/格式转换 -> 原生绘制播放 -> 音视频同步 )



II . FFMPEG 下载及交叉编译

1 . FFMPEG 下载 :


① FFMPEG 源码下载地址 : http://ffmpeg.org/download.html#releases

② 本博客使用的源码下载地址 : https://ffmpeg.org/releases/ffmpeg-4.0.2.tar.bz2
( 也可以直接从博客资源中下载 )


2 . FFMPEG 编译 : 在 Ubuntu 18.04.4 中解压该源码 ;


① 配置编译脚本 : 在 FFMPEG 源码根目录下 , 创建 build_ffmpeg.sh 文件 , 内容如下 ;

#!/bin/bash
# NDK 根目录
NDK_ROOT=/root/NDK/android-ndk-r17c
# TOOLCHAIN 变量指向 gcc g++ 等交叉编译工具所在的目录
TOOLCHAIN=$NDK_ROOT/toolchains/arm-linux-androideabi-4.9/prebuilt/linux-x86_64
# gcc 编译器参数
FLAGS="-isysroot $NDK_ROOT/sysroot -isystem $NDK_ROOT/sysroot/usr/include/arm-linux-androideabi -isystem $NDK_ROOT/sources/android/support/include -D__ANDROID_API__=21 -g -DANDROID -ffunction-sections -funwind-tables -fstack-protector-strong -no-canonical-prefixes -march=armv7-a -mfloat-abi=softfp -mfpu=vfpv3-d16 -mthumb -Wa,--noexecstack -Wformat -Werror=format-security -O0 -fPIC"
# 编译结果输出路径
PREFIX=./android/armeabi-v7a
# 执行 configure 脚本生成 Makefile 构建脚本
./configure \
--prefix=$PREFIX \
--enable-small \
--disable-programs \
--disable-avdevice \
--disable-encoders \
--disable-muxers \
--disable-filters \
--enable-cross-compile \
--cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
--sysroot=$NDK_ROOT/platforms/android-21/arch-arm \
--extra-cflags="$FLAGS" \
--arch=arm \
--target-os=android 
# 清除之前的编译内容
make clean
# 开启新的 FFMPEG 编译
make install

② 修改 Shell 脚本权限 :

chmod +x build_ffmpeg.sh

③ 执行 Shell 脚本 :

./build_ffmpeg.sh

④ 编译结果 :


【Android FFMPEG 开发】Android Studio 工程配置 FFMPEG ( 动态库打包 | 头文件与函数库拷贝 | CMake 脚本配置 )



IV . FFMPEG 初始化

1 . FFMPEG 初始化流程 : FFMPEG 执行任何操作前 , 都需要初始化一些环境 , 及相关数据参数 ;


① 网络初始化 : avformat_network_init()

int avformat_network_init(void);

② 打开媒体 ( 音视频 ) 地址 : avformat_open_input()

int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);

③ 查找 ( 音 / 视频 ) 流 : avformat_find_stream_info()

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

④ 正式操作 : 对上述查找到的 音 / 视频 流进行操作 ;



2 . 代码示例 :

    
    avformat_network_init();
    //0 . 注册组件
    //      如果是 4.x 之前的版本需要执行该步骤
    //      4.x 及之后的版本 , 就没有该步骤了
    //av_register_all();
    //1 . 打开音视频地址 ( 播放文件前 , 需要先将文件打开 )
    //      地址类型 : ① 文件类型 , ② 音视频流
    //  参数解析 :
    //      AVFormatContext **ps :  封装了文件格式相关信息的结构体 , 如视频宽高 , 音频采样率等信息 ;
    //                              该参数是 二级指针 , 意味着在方法中会修改该指针的指向 ,
    //                              该参数的实际作用是当做返回值用的
    //      const char *url :   视频资源地址, 文件地址 / 网络链接
    //  返回值说明 : 返回 0 , 代表打开成功 , 否则失败
    //              失败的情况 : 文件路径错误 , 网络错误
    //int avformat_open_input(AVFormatContext **ps, const char *url,
    //                          AVInputFormat *fmt, AVDictionary **options);
    formatContext = 0;
    int open_result = avformat_open_input(&formatContext, dataSource, 0, 0);
    //如果返回值不是 0 , 说明打开视频文件失败 , 需要将错误信息在 Java 层进行提示
    //  这里将错误码返回到 Java 层显示即可
    if(open_result != 0){
        __android_log_print(ANDROID_LOG_ERROR , "FFMPEG" , "打开媒体失败 : %s", av_err2str(open_result));
        callHelper->onError(pid, 0);
    }
    //2 . 查找媒体 地址 对应的音视频流 ( 给 AVFormatContext* 成员赋值 )
    //      方法原型 : int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
    //      调用该方法后 , AVFormatContext 结构体的 nb_streams 元素就有值了 ,
    //      该值代表了音视频流 AVStream 个数
    int find_result = avformat_find_stream_info(formatContext, 0);
    //如果返回值 < 0 , 说明查找音视频流失败 , 需要将错误信息在 Java 层进行提示
    //  这里将错误码返回到 Java 层显示即可
    if(find_result onError(pid, 1);
    }

【Android FFMPEG 开发】FFMPEG 初始化 ( 网络初始化 | 打开音视频 | 查找音视频流 )



V . FFMPEG 获取 AVStream 音视频流

1 . FFMPEG 音视频流 AVStream ( 结构体 ) 获取流程 :


① 获取音视频流信息 : avformat_find_stream_info ( ) , 在 【Android FFMPEG 开发】FFMPEG 初始化 ( 网络初始化 | 打开音视频 | 查找音视频流 ) 博客中 , FFMPEG 初始化完毕后 , 获取了音视频流 , 本博客中讲解获取该音视频流对应的编解码器 , 从获取该音视频流开始 ;

int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);

② 音视频流数量 : 获取的音视频流信息存储在 AVFormatContext *formatContext 结构体中 , nb_streams 元素的值就是音视频流的个数 ;

//音视频流的个数
formatContext->nb_streams

③ 音视频流 : AVFormatContext *formatContext 结构体中的 音视频流数组元素 AVStream **streams 元素 , 通过数组下标可以获取指定位置索引的音视频流 ;

//取出一个媒体流 ( 视频流 / 音频流 )
AVStream *stream = formatContext->streams[i];


2 . 代码示例 :

    //2 . 查找媒体 地址 对应的音视频流 ( 给 AVFormatContext* 成员赋值 )
    //      方法原型 : int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
    //      调用该方法后 , AVFormatContext 结构体的 nb_streams 元素就有值了 ,
    //      该值代表了音视频流 AVStream 个数
    int find_result = avformat_find_stream_info(formatContext, 0);
    //如果返回值 < 0 , 说明查找音视频流失败 , 需要将错误信息在 Java 层进行提示
    //  这里将错误码返回到 Java 层显示即可
    if(find_result onError(pid, 1);
    }
    //3 . 处理视频流 , 解析其中的数据流 , 获取流的各种参数 , 编解码器 , 等信息
    //      为视频 音频 解码播放准备数据
    //formatContext->nb_streams 是 音频流 / 视频流 个数 ;
    //  循环解析 视频流 / 音频流 , 一般是两个 , 一个视频流 , 一个音频流
    for(int i = 0; i nb_streams; i ++){
        //取出一个媒体流 ( 视频流 / 音频流 )
        AVStream *stream = formatContext->streams[i];
     }

【Android FFMPEG 开发】FFMPEG 获取 AVStream 音视频流 ( AVFormatContext 结构体 | 获取音视频流信息 | 获取音视频流个数 | 获取音视频流 )



VI . FFMPEG 获取编解码器

1 . FFMPEG 编解码器获取流程 : 在获取音视频流 AVStream *stream 之后 , 执行以下流程 ;


〇 获取 AVStream * 音视频流 ( 获取编解码器前提 ) : 参考博客 【Android FFMPEG 开发】FFMPEG 获取 AVStream 音视频流 ( AVFormatContext 结构体 | 获取音视频流信息 | 获取音视频流个数 | 获取音视频流 )


① 获取音视频流的编码参数 : AVStream *stream 结构体的 AVCodecParameters *codecpar 元素是音视频流的编解码参数 ; 包含 码率 , 宽度 , 高度 , 采样率 等参数信息 ;

//解码这个媒体流的参数信息 , 包含 码率 , 宽度 , 高度 , 采样率 等参数信息
AVCodecParameters *codecParameters = stream->codecpar;

② 查找编解码器 : 调用 avcodec_find_decoder ( ) 获取当前音视频流使用的编解码器 ;

//① 查找 当前流 使用的编码方式 , 进而查找编解码器 ( 可能失败 , 不支持的解码方式 )
AVCodec *avCodec = avcodec_find_decoder(codecParameters->codec_id);

③ 获取编解码器上下文 : 调用 avcodec_alloc_context3 ( ) 方法 , 获取编解码器上下文 ;

//② 获取编解码器上下文
AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);

④ 设置编解码器上下文参数 : 调用 avcodec_parameters_to_context ( ) 方法 , 设置编解码器的上下文参数 ;

//③ 设置 编解码器上下文 参数
//      int avcodec_parameters_to_context(AVCodecContext *codec,
//              const AVCodecParameters *par);
//      返回值 > 0 成功 , < 0 失败
int parameters_to_context_result =
        avcodec_parameters_to_context(avCodecContext, codecParameters);

⑤ 打开编解码器 : 调用 avcodec_open2 ( ) 方法 , 打开编解码器 ;

//④ 打开编解码器
//   int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, 
//   返回 0 成功 , 其它失败
int open_codec_result = avcodec_open2(avCodecContext, avCodec, 0);


2 . 代码示例 :

        //视频 / 音频 处理需要的操作 ( 获取编解码器 )
        //① 查找 当前流 使用的编码方式 , 进而查找编解码器 ( 可能失败 , 不支持的解码方式 )
        AVCodec *avCodec = avcodec_find_decoder(codecParameters->codec_id);
        //查找失败处理
        if(avCodec == NULL){
            //如果没有找到编解码器 , 回调失败 , 方法直接返回 , 后续代码不执行
            callHelper->onError(pid, 2);
            __android_log_print(ANDROID_LOG_ERROR , "FFMPEG" , "查找 编解码器 失败");
            return;
        }
        //② 获取编解码器上下文
        AVCodecContext *avCodecContext = avcodec_alloc_context3(avCodec);
        //获取编解码器失败处理
        if(avCodecContext == NULL){
            callHelper->onError(pid, 3);
            __android_log_print(ANDROID_LOG_ERROR , "FFMPEG" , "创建编解码器上下文 失败");
            return;
        }
        //③ 设置 编解码器上下文 参数
        //      int avcodec_parameters_to_context(AVCodecContext *codec,
        //              const AVCodecParameters *par);
        //      返回值 > 0 成功 , < 0 失败
        int parameters_to_context_result =
                avcodec_parameters_to_context(avCodecContext, codecParameters);
        //设置 编解码器上下文 参数 失败处理
        if(parameters_to_context_result onError(pid, 4);
            __android_log_print(ANDROID_LOG_ERROR , "FFMPEG" , "设置编解码器上下文参数 失败");
            return;
        }
        //④ 打开编解码器
        //   int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
        //   返回 0 成功 , 其它失败
        int open_codec_result = avcodec_open2(avCodecContext, avCodec, 0);
        //打开编解码器 失败处理
        if(open_codec_result != 0){
            callHelper->onError(pid, 5);
            __android_log_print(ANDROID_LOG_ERROR , "FFMPEG" , "打开 编解码器 失败");
            return;
        }

【Android FFMPEG 开发】FFMPEG 获取编解码器 ( 获取编解码参数 | 查找编解码器 | 获取编解码器上下文 | 设置上下文参数 | 打开编解码器 )



VII . FFMPEG 读取音视频流中的数据到 AVPacket ( 压缩编码后的数据包 )

1 . FFMPEG 获取 AVPacket 数据流程 :


〇 前置操作 : FFMPEG 环境初始化 , 获取 AVStream 音视频流 , 获取 AVCodec 编解码器 , 然后才能进行下面的操作 ;


① 初始化 AVPacket 空数据包 : av_packet_alloc ( )

AVPacket *avPacket = av_packet_alloc();

② 读取 AVPacket 数据 : av_read_frame ( AVFormatContext *s , AVPacket *pkt )

int read_frame_result = av_read_frame(formatContext, avPacket);


2 . 代码示例 :

//读取数据包
// AVPacket 存放编码后的音视频数据的 , 获取该数据包后 , 需要对该数据进行解码 , 解码后将数据存放在 AVFrame 中
// AVPacket 是编码后的数据 , AVFrame 是编码前的数据
//创建 AVPacket 空数据包
AVPacket *avPacket = av_packet_alloc();

int read_frame_result = av_read_frame(formatContext, avPacket);

【Android FFMPEG 开发】FFMPEG 读取音视频流中的数据到 AVPacket ( 初始化 AVPacket 数据 | 读取 AVPacket )



VIII . FFMPEG AVFrame 图像格式转换 YUV -> RGBA

1 . FFMPEG 解码 AVPacket 数据到 AVFrame 流程 :


〇 前置操作 : FFMPEG 环境初始化 , 获取 AVStream 音视频流 , 获取 AVCodec 编解码器 , 读取音视频流中的数据到 AVPacket , 解码 AVPacket 数据到 AVFrame , 然后才能进行下面的操作 ;


① 获取 SwsContext : sws_getContext ( )

SwsContext *swsContext = sws_getContext(
        //源图像的 宽 , 高 , 图像像素格式
        avCodecContext->width, avCodecContext->height, avCodecContext->pix_fmt,
        //目标图像 大小不变 , 不进行缩放操作 , 只将像素格式设置成 RGBA 格式的
        avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,
        //使用的转换算法 , FFMPEG 提供了许多转换算法 , 有快速的 , 有高质量的 , 需要自己测试
        SWS_BILINEAR,
        //源图像滤镜 , 这里传 NULL 即可
        0,
        //目标图像滤镜 , 这里传 NULL 即可
        0,
        //额外参数 , 这里传 NULL 即可
        0
        );

② 初始化图像数据存储空间 : av_image_alloc ( )

av_image_alloc(dst_data, dst_linesize,
               avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,
               1);

③ 转换图像格式 : sws_scale ( )

sws_scale(
        //SwsContext *swsContext 转换上下文
        swsContext,
        //要转换的数据内容
        avFrame->data,
        //数据中每行的字节长度
        avFrame->linesize,
        0,
        avFrame->height,
        //转换后目标图像数据存放在这里
        dst_data,
        //转换后的目标图像行数
        dst_linesize
        );


2 . 代码示例 :

//1 . 获取转换上下文
SwsContext *swsContext = sws_getContext(
        //源图像的 宽 , 高 , 图像像素格式
        avCodecContext->width, avCodecContext->height, avCodecContext->pix_fmt,
        //目标图像 大小不变 , 不进行缩放操作 , 只将像素格式设置成 RGBA 格式的
        avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,
        //使用的转换算法 , FFMPEG 提供了许多转换算法 , 有快速的 , 有高质量的 , 需要自己测试
        SWS_BILINEAR,
        //源图像滤镜 , 这里传 NULL 即可
        0,
        //目标图像滤镜 , 这里传 NULL 即可
        0,
        //额外参数 , 这里传 NULL 即可
        0
        );
//2 . 初始化图像存储内存
//指针数组 , 数组中存放的是指针
uint8_t *dst_data[4];
//普通的 int 数组
int dst_linesize[4];
//初始化 dst_data 和 dst_linesize , 为其申请内存 , 注意使用完毕后需要释放内存
av_image_alloc(dst_data, dst_linesize,
               avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,
               1);
//3 . 格式转换
sws_scale(
        //SwsContext *swsContext 转换上下文
        swsContext,
        //要转换的数据内容
        avFrame->data,
        //数据中每行的字节长度
        avFrame->linesize,
        0,
        avFrame->height,
        //转换后目标图像数据存放在这里
        dst_data,
        //转换后的目标图像行数
        dst_linesize
        );

【Android FFMPEG 开发】FFMPEG AVFrame 图像格式转换 YUV -> RGBA ( 获取 SwsContext | 初始化图像数据存储内存 | 图像格式转换 )



IX . ANativeWindow 原生绘制

FFMPEG 解码 AVPacket 数据到 AVFrame 流程 :


〇 前置操作 : FFMPEG 环境初始化 , 获取 AVStream 音视频流 , 获取 AVCodec 编解码器 , 读取音视频流中的数据到 AVPacket , 解码 AVPacket 数据到 AVFrame , AVFrame 图像格式转换 YUV -> RGBA , 然后才能进行下面的操作 ;


① Java 层获取 Surface 对象 : Surface 画布可以在 SurfaceView 的 SurfaceHolder 中获取

//绘制图像的 SurfaceView
SurfaceView surfaceView;
//在 SurfaceView 回调函数中获取
SurfaceHolder surfaceHolder = surfaceView.getHolder() ; 
//获取 Surface 画布
Surface surface = surfaceHolder.getSurface() ;

② 将 Surface 对象传递到 Native 层 : 在 SurfaceHolder.Callback 接口的 surfaceChanged 实现方法中 , 将 Surface 画布传递给 Native 层 ;

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    //画布改变 , 横竖屏切换 , 按下 Home 键 , 按下菜单键
    //将 Surface 传到 Native 层 , 在 Native 层绘制图像
    native_set_surface(holder.getSurface());
}
//调用该方法将 Surface 传递到 Native 层
native void native_set_surface(Surface surface);

③ 设置 ANativeWindow 绘制缓冲区属性 : ANativeWindow_setBuffersGeometry ( )

//设置 ANativeWindow 绘制窗口属性
//  传入的参数分别是 : ANativeWindow 结构体指针 , 图像的宽度 , 图像的高度 , 像素的内存格式
ANativeWindow_setBuffersGeometry(aNativeWindow, width, height, WINDOW_FORMAT_RGBA_8888);

④ 获取 ANativeWindow_Buffer 绘制缓冲区 : ANativeWindow_lock ( )

//获取 ANativeWindow_Buffer , 如果获取失败 , 直接释放相关资源退出
ANativeWindow_Buffer aNativeWindow_Buffer;
//如果获取成功 , 可以继续向后执行 , 获取失败 , 直接退出
if(ANativeWindow_lock(aNativeWindow, &aNativeWindow_Buffer, 0)){
    //退出操作 , 释放 aNativeWindow 结构体指针
    ANativeWindow_release(aNativeWindow);
    aNativeWindow = 0;
    return;
}

⑤ 填充图像数据到 ANativeWindow_Buffer 绘制缓冲区中 : 将图像字节数据使用内存拷贝到 ANativeWindow_Buffer 结构体的 bits 字段中 ;

//向 ANativeWindow_Buffer 填充 RGBA 像素格式的图像数据
uint8_t *dst_data = static_cast(aNativeWindow_Buffer.bits);
//参数中的 uint8_t *data 数据中 , 每一行有 linesize 个 , 拷贝的目标也要逐行拷贝
//  aNativeWindow_Buffer.stride 是每行的数据个数 , 每个数据都包含一套 RGBA 像素数据 ,
//      RGBA 数据每个占1字节 , 一个 RGBA 占 4 字节
//  每行的数据个数 * 4 代表 RGBA 数据个数
int dst_linesize = aNativeWindow_Buffer.stride * 4;
//获取 ANativeWindow_Buffer 中数据的地址
//      一次拷贝一行 , 有 像素高度 行数
for(int i = 0; i < aNativeWindow_Buffer.height; i++){
    //计算拷贝的指针地址
    //  每次拷贝的目的地址 : dst_data + ( i * dst_linesize )
    //  每次拷贝的源地址 : data + ( i * linesize )
    memcpy(dst_data + ( i * dst_linesize ), data + ( i * linesize ), dst_linesize);
}

⑥ 启动绘制 : ANativeWindow_unlockAndPost ( )

//启动绘制
ANativeWindow_unlockAndPost(aNativeWindow);


2 . 代码示例 :

// I . 图像格式转换
//指针数组 , 数组中存放的是指针
uint8_t *dst_data[4];
//普通的 int 数组
int dst_linesize[4];
//初始化 dst_data 和 dst_linesize , 为其申请内存 , 注意使用完毕后需要释放内存
av_image_alloc(dst_data, dst_linesize,
               avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,
               1);
//3 . 格式转换
sws_scale(
        //SwsContext *swsContext 转换上下文
        swsContext,
        //要转换的数据内容
        avFrame->data,
        //数据中每行的字节长度
        avFrame->linesize,
        0,
        avFrame->height,
        //转换后目标图像数据存放在这里
        dst_data,
        //转换后的目标图像行数
        dst_linesize
        );
// II . 拷贝图像数据
//1 . 向 ANativeWindow_Buffer 填充 RGBA 像素格式的图像数据
uint8_t *dst_data = static_cast(aNativeWindow_Buffer.bits);
//2 . 参数中的 uint8_t *data 数据中 , 每一行有 linesize 个 , 拷贝的目标也要逐行拷贝
//  aNativeWindow_Buffer.stride 是每行的数据个数 , 每个数据都包含一套 RGBA 像素数据 ,
//      RGBA 数据每个占1字节 , 一个 RGBA 占 4 字节
//  每行的数据个数 * 4 代表 RGBA 数据个数
int dst_linesize = aNativeWindow_Buffer.stride * 4;
//3 . 获取 ANativeWindow_Buffer 中数据的地址
//      一次拷贝一行 , 有 像素高度 行数
for(int i = 0; i < aNativeWindow_Buffer.height; i++){
    //计算拷贝的指针地址
    //  每次拷贝的目的地址 : dst_data + ( i * dst_linesize )
    //  每次拷贝的源地址 : data + ( i * linesize )
    memcpy(dst_data + ( i * dst_linesize ), data + ( i * linesize ), dst_linesize);
}
// III . 启动绘制
//启动绘制
ANativeWindow_unlockAndPost(aNativeWindow);

【Android FFMPEG 开发】FFMPEG ANativeWindow 原生绘制 ( Java 层获取 Surface | 传递画布到本地 | 创建 ANativeWindow )
【Android FFMPEG 开发】FFMPEG ANativeWindow 原生绘制 ( 设置 ANativeWindow 缓冲区属性 | 获取绘制缓冲区 | 填充数据到缓冲区 | 启动绘制 )



X . FFMPEG 音频重采样

1 . FFMPEG 音频重采样流程 :


〇 视频播放操作 : FFMPEG 环境初始化 , 获取 AVStream 音视频流 , 获取 AVCodec 编解码器 , 读取音视频流中的数据到 AVPacket , 解码 AVPacket 数据到 AVFrame , AVFrame 图像格式转换 YUV -> RGBA , ANativeWindow 原生绘制 ;


〇 音频播放操作 : FFMPEG 环境初始化 , 获取 AVStream 音视频流 , 获取 AVCodec 编解码器 , 读取音视频流中的数据到 AVPacket , 解码 AVPacket 数据到 AVFrame , 然后进行下面的操作 , 音频重采样 ;


① 初始化音频重采样上下文 : struct SwrContext *swr_alloc_set_opts( … ) , int swr_init(struct SwrContext *s)

SwrContext *swrContext = swr_alloc_set_opts(
        0 ,                     //现在还没有 SwrContext 上下文 , 先传入 0
        //输出的音频参数
        AV_CH_LAYOUT_STEREO ,   //双声道立体声
        AV_SAMPLE_FMT_S16 ,     //采样位数 16 位
        44100 ,                 //输出的采样率
        //从编码器中获取输入音频格式
        avCodecContext->channel_layout, //输入的声道数
        avCodecContext->sample_fmt,     //输入的采样位数
        avCodecContext->sample_rate,    //输入的采样率
        0, 0    //日志参数 设置 0 即可
        );
swr_init(swrContext);

② 计算积压的延迟数据 : int64_t swr_get_delay(struct SwrContext *s, int64_t base)

int64_t delay = swr_get_delay(swrContext , avFrame->sample_rate);

③ 计算本次重采样后的样本个数 : int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding rnd) av_const

int64_t out_count = av_rescale_rnd(
        avFrame->nb_samples + delay, //本次要处理的数据个数
        44100,
        avFrame->sample_rate ,
        AV_ROUND_UP );

④ 音频重采样 : int swr_convert(struct SwrContext *s, uint8_t **out, int out_count, const uint8_t **in , int in_count)

int samples_per_channel_count = swr_convert(
        swrContext ,
        &data,
        out_count ,
        (const uint8_t **)avFrame->data, //普通指针转为 const 指针需要使用 const_cast 转换
        avFrame->nb_samples
        );

⑤ 计算音频重采样字节数 : 音频重采样 swr_convert ( ) 返回值 samples_per_channel_count 是 每个通道的样本数 ;

pcm_data_bit_size = samples_per_channel_count * 2 * 2;


2 . 代码示例 :

// I . 音频重采样输出缓冲区准备

uint8_t *data = static_cast(malloc(44100 * 2 * 2)); 
//初始化内存数据
memset(data, 0, 44100 * 2 * 2);
// II . 音频重采样上下文 初始化

swrContext = swr_alloc_set_opts(
        0 ,                     //现在还没有 SwrContext 上下文 , 先传入 0
        //输出的音频参数
        AV_CH_LAYOUT_STEREO ,   //双声道立体声
        AV_SAMPLE_FMT_S16 ,     //采样位数 16 位
        44100 ,                 //输出的采样率
        //从编码器中获取输入音频格式
        avCodecContext->channel_layout, //输入的声道数
        avCodecContext->sample_fmt,     //输入的采样位数
        avCodecContext->sample_rate,    //输入的采样率
        0, 0    //日志参数 设置 0 即可
        );
//注意创建完之后初始化
swr_init(swrContext);
// III . 获取延迟数据
//OpenSLES 播放器设定播放的音频格式是 立体声 , 44100 Hz 采样 , 16位采样位数
//  解码出来的 AVFrame 中的数据格式不确定 , 需要进行重采样

int64_t delay = swr_get_delay(swrContext , avFrame->sample_rate);
// IV . 计算输出样本个数

int64_t out_count = av_rescale_rnd(
        avFrame->nb_samples + delay, //本次要处理的数据个数
        44100,
        avFrame->sample_rate ,
        AV_ROUND_UP );
// V . 音频重采样

int samples_per_channel_count = swr_convert(
        swrContext ,
        &data,
        out_count ,
        (const uint8_t **)avFrame->data, //普通指针转为 const 指针需要使用 const_cast 转换
        avFrame->nb_samples
        );
// VI . 最终重采样后的数据字节大小
//根据样本个数计算样本的字节数
pcm_data_bit_size = samples_per_channel_count * 2 * 2;

【Android FFMPEG 开发】FFMPEG 音频重采样 ( 初始化音频重采样上下文 SwrContext | 计算音频延迟 | 计算输出样本个数 | 音频重采样 swr_convert )



XI . OpenSLES 播放音频

1 . OpenSLES 播放音频流程 :


〇 视频播放操作 : FFMPEG 环境初始化 , 获取 AVStream 音视频流 , 获取 AVCodec 编解码器 , 读取音视频流中的数据到 AVPacket , 解码 AVPacket 数据到 AVFrame , AVFrame 图像格式转换 YUV -> RGBA , ANativeWindow 原生绘制 ;


〇 音频播放操作 : FFMPEG 环境初始化 , 获取 AVStream 音视频流 , 获取 AVCodec 编解码器 , 读取音视频流中的数据到 AVPacket , 解码 AVPacket 数据到 AVFrame , 音频重采样 , 然后使用 OpenSLES 播放重采样后的音频 ;


① 创建引擎 : 先创建引擎对象 , 再实现引擎对象 , 最后从引擎对象中 , 获取引擎接口 ;

SLresult result;
// 创建引擎
result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
// 实现引擎
result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
// 获取引擎接口
result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);

② 设置输出混音器 : 创建输出混音器对象 , 实现输出混音器 ;

// 创建输出混音器对象 , 可以指定一个混响效果参数 ( 该混淆参数可选 )
const SLInterfaceID ids_engine[1] = {SL_IID_ENVIRONMENTALREVERB};
const SLboolean req_engine[1] = {SL_BOOLEAN_FALSE};
result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids_engine, req_engine);
// 实现输出混音器
result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);

③ 获取混响接口并设置混响 : 该步骤不是必须操作 , 另外获取混响接口可能失败 ;

// 获取混响接口
result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
                                           &outputMixEnvironmentalReverb);
// 设置混响
if (SL_RESULT_SUCCESS == result) {
    result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
            outputMixEnvironmentalReverb, &reverbSettings);
    (void)result;
}

④ 配置音源输入 : 配置音频数据源缓冲队列 , 和 音源格式 ( 采样率 , 样本位数 , 通道数 , 样本大小端格式 ) ;

//1 . 配置音源输入
// 配置要播放的音频输入缓冲队列属性参数 , 缓冲区大小 , 音频格式 , 采样率 , 样本位数 , 通道数 , 样本大小端格式
SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
// PCM 格式
SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM,           //PCM 格式
                               2,                           //两个声道
                               SL_SAMPLINGRATE_44_1,        //采样率 44100 Hz
                               SL_PCMSAMPLEFORMAT_FIXED_16, //采样位数 16位
                               SL_PCMSAMPLEFORMAT_FIXED_16, //容器为 16 位
                               SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,  //左右双声道
                               SL_BYTEORDER_LITTLEENDIAN};  //小端格式
// 设置音频数据源 , 配置缓冲区 ( loc_bufq ) 与 音频格式 (format_pcm)
SLDataSource audioclass="lazy" data-src = {&loc_bufq, &format_pcm};

⑤ 配置音频输出 : 装载输出混音器对象 到 SLDataLocator_OutputMix , 在将 SLDataLocator_OutputMix 结构体装载到 SLDataSink 中 ;

// 配置混音器 : 将 outputMixObject 混音器对象装载入 SLDataLocator_OutputMix 结构体中
SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
// 将 SLDataLocator_OutputMix 结构体装载到 SLDataSink 中
//  音频输出通过 loc_outmix 输出 , 实际上是通过 outputMixObject 混音器对象输出的
SLDataSink audioSnk = {&loc_outmix, NULL};

⑥ 创建并实现播放器 : 先使用 引擎 , 音源输入 , 音频输出 , 采样率 , 接口队列ID 等参数创建播放器 , 再实现播放器对象 ;

// 操作队列接口 , 如果需要 特效接口 , 添加 SL_IID_EFFECTSEND
const SLInterfaceID ids_player[3] = {SL_IID_BUFFERQUEUE, SL_IID_VOLUME, SL_IID_EFFECTSEND,
        };
const SLboolean req_player[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE,
         };
// 创建播放器
result = (*engineEngine)->CreateAudioPlayer(
        engineEngine,
        &bqPlayerObject,
        &audioclass="lazy" data-src, //音频输入
        &audioSnk, //音频商户处
        bqPlayerSampleRate? 2 : 3,//
        ids_player,
        req_player);
// 创建播放器对象
result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);

⑦ 获取播放器接口 和 缓冲队列接口 : 获取的接口 对应 播放器创建时的接口 ID 数组参数 ;

// 获取播放器 Player 接口 : 该接口用于设置播放器状态 , 开始 暂停 停止 播放 等操作
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
// 获取播放器 缓冲队列 接口 : 该接口用于控制 音频 缓冲区数据 播放
result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
                                         &bqPlayerBufferQueue);

⑧ 注册回调函数 : 按照指定的回调函数类型 , 声明并实现该回调函数 , 并将该回调函数注册给播放器缓冲队列接口 ;

// 注册缓冲区队列的回调函数 , 每次播放完数据后 , 会自动回调该函数
//      传入参数 this , 就是 bqPlayerCallback 函数中的 context 参数
result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, this);

回调函数类型 :

typedef void (SLAPIENTRY *slAndroidSimpleBufferQueueCallback)(
	SLAndroidSimpleBufferQueueItf caller,
	void *pContext
);

回调函数实现 :

//每当缓冲数据播放完毕后 , 会自动回调该回调函数
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
	...
    //通过播放器队列接口 , 将 PCM 数据加入到该队列缓冲区后 , 就会自动播放这段音频
    (*bq)->Enqueue(bq, audioChannel->data, data_size);
}

⑨ 获取效果器接口 和 音量控制接口 : 这两个接口不是必须的 , 可选选项 ;

    // 获取效果器发送接口 ( get the effect send interface )
    bqPlayerEffectSend = NULL;
    if( 0 == bqPlayerSampleRate) {
        result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_EFFECTSEND,
                                                 &bqPlayerEffectSend);
    }
    // 获取音量控制接口 ( get the volume interface ) [ 如果需要调节音量可以获取该接口 ]
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);

⑩ 设置播放状态 : 设置播放状态为 SL_PLAYSTATE_PLAYING ;

// 设置播放器正在播放状态 ( set the player's state to playing )
result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);

⑪ 手动调用激活回调函数 : 第一次激活回调函数调用 , 需要手动激活 ;

// 手动激活 , 手动调用一次 bqPlayerCallback 回调函数
bqPlayerCallback(bqPlayerBufferQueue, this);


2 . 代码示例 :

    // I . 创建 OpenSLES 引擎并获取引擎的接口 ( 相关代码拷贝自 Google 官方示例 native-audio )
    //      参考 : https://github.com/android/ndk-samples/blob/master/native-audio/app/class="lazy" data-src/main/cpp/native-audio-jni.c
    //声明每个方法执行的返回结果 , 一般情况下返回 SL_RESULT_SUCCESS 即执行成功
    //  该类型本质是 int 类型 , 定义的是各种类型的异常
    SLresult result;
    // 创建引擎
    result = slCreateEngine(&engineObject, 0, NULL, 0, NULL, NULL);
    // 返回 0 成功 , 否则失败 , 一旦失败就中断退出
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // 实现引擎
    result = (*engineObject)->Realize(engineObject, SL_BOOLEAN_FALSE);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // 获取引擎接口 , 使用该接口创建输出混音器 , 音频播放器等其它对象
    //      引擎对象不提供任何调用的方法 , 引擎调用的方法都定义在接口中
    result = (*engineObject)->GetInterface(engineObject, SL_IID_ENGINE, &engineEngine);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // II . 设置输出混音器
    //  输出声音 , 添加各种音效 ( 混响 , 重低音 , 环绕音 , 均衡器 等 ) , 都要通过混音器实现 ;
    // 创建输出混音器对象 , 可以指定一个混响效果参数 ( 该混淆参数可选 )
    const SLInterfaceID ids_engine[1] = {SL_IID_ENVIRONMENTALREVERB};
    const SLboolean req_engine[1] = {SL_BOOLEAN_FALSE};
    result = (*engineEngine)->CreateOutputMix(engineEngine, &outputMixObject, 1, ids_engine, req_engine);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // 实现输出混音器
    result = (*outputMixObject)->Realize(outputMixObject, SL_BOOLEAN_FALSE);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // III . 获取混响接口 并 设置混响 ( 可能会失败 )
    // 获取环境混响接口
    // 如果环境混响效果不可用 , 该操作可能失败
    // either because the feature is not present, excessive CPU load, or
    // the required MODIFY_AUDIO_SETTINGS permission was not requested and granted
    result = (*outputMixObject)->GetInterface(outputMixObject, SL_IID_ENVIRONMENTALREVERB,
                                               &outputMixEnvironmentalReverb);
    if (SL_RESULT_SUCCESS == result) {
        result = (*outputMixEnvironmentalReverb)->SetEnvironmentalReverbProperties(
                outputMixEnvironmentalReverb, &reverbSettings);
        (void)result;
    }
    //IV . 配置音源输入
    // 配置要播放的音频输入缓冲队列属性参数 , 缓冲区大小 , 音频格式 , 采样率 , 样本位数 , 通道数 , 样本大小端格式
    SLDataLocator_AndroidSimpleBufferQueue loc_bufq = {SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, 2};
    
    SLDataFormat_PCM format_pcm = {SL_DATAFORMAT_PCM,           //PCM 格式
                                   2,                           //两个声道
                                   SL_SAMPLINGRATE_44_1,        //采样率 44100 Hz
                                   SL_PCMSAMPLEFORMAT_FIXED_16, //采样位数 16位
                                   SL_PCMSAMPLEFORMAT_FIXED_16, //容器为 16 位
                                   SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,  //左右双声道
                                   SL_BYTEORDER_LITTLEENDIAN};  //小端格式
    // 设置音频数据源 , 配置缓冲区 ( loc_bufq ) 与 音频格式 (format_pcm)
    SLDataSource audioclass="lazy" data-src = {&loc_bufq, &format_pcm};
    // V . 配置音频输出
    // 配置混音器 : 将 outputMixObject 混音器对象装载入 SLDataLocator_OutputMix 结构体中
    SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, outputMixObject};
    // 将 SLDataLocator_OutputMix 结构体装载到 SLDataSink 中
    //  音频输出通过 loc_outmix 输出 , 实际上是通过 outputMixObject 混音器对象输出的
    SLDataSink audioSnk = {&loc_outmix, NULL};
    // VI . 创建并实现播放器
    
    // 操作队列接口 , 如果需要 特效接口 , 添加 SL_IID_EFFECTSEND
    const SLInterfaceID ids_player[3] = {SL_IID_BUFFERQUEUE, SL_IID_VOLUME, SL_IID_EFFECTSEND,
            };
    const SLboolean req_player[3] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE,
             };
    // 创建播放器
    result = (*engineEngine)->CreateAudioPlayer(
            engineEngine,
            &bqPlayerObject,
            &audioclass="lazy" data-src, //音频输入
            &audioSnk, //音频商户处
            bqPlayerSampleRate? 2 : 3,//
            ids_player,
            req_player);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // 创建播放器对象
    result = (*bqPlayerObject)->Realize(bqPlayerObject, SL_BOOLEAN_FALSE);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // VII . 获取播放器接口 和 缓冲队列接口
    // 获取播放器 Player 接口 : 该接口用于设置播放器状态 , 开始 暂停 停止 播放 等操作
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_PLAY, &bqPlayerPlay);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // 获取播放器 缓冲队列 接口 : 该接口用于控制 音频 缓冲区数据 播放
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_BUFFERQUEUE,
                                             &bqPlayerBufferQueue);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // VIII . 注册回调函数
    // 注册缓冲区队列的回调函数 , 每次播放完数据后 , 会自动回调该函数
    //      传入参数 this , 就是 bqPlayerCallback 函数中的 context 参数
    result = (*bqPlayerBufferQueue)->RegisterCallback(bqPlayerBufferQueue, bqPlayerCallback, this);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // IX . 获取效果器接口 和 音量控制接口 ( 不是必须的 )
    // 获取效果器发送接口 ( get the effect send interface )
    bqPlayerEffectSend = NULL;
    if( 0 == bqPlayerSampleRate) {
        result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_EFFECTSEND,
                                                 &bqPlayerEffectSend);
        assert(SL_RESULT_SUCCESS == result);
        (void)result;
    }
#if 0   // mute/solo is not supported for sources that are known to be mono, as this is
    // get the mute/solo interface
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_MUTESOLO, &bqPlayerMuteSolo);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
#endif
    // 获取音量控制接口
    // 获取音量控制接口 ( get the volume interface ) [ 如果需要调节音量可以获取该接口 ]
    result = (*bqPlayerObject)->GetInterface(bqPlayerObject, SL_IID_VOLUME, &bqPlayerVolume);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // X . 设置播放状态
    // 设置播放器正在播放状态 ( set the player's state to playing )
    result = (*bqPlayerPlay)->SetPlayState(bqPlayerPlay, SL_PLAYSTATE_PLAYING);
    assert(SL_RESULT_SUCCESS == result);
    (void)result;
    // XI. 手动调用激活回调函数
    // 手动激活 , 手动调用一次 bqPlayerCallback 回调函数
    bqPlayerCallback(bqPlayerBufferQueue, this);

3 . bqPlayerCallback 回调函数 :

//每当缓冲数据播放完毕后 , 会自动回调该回调函数
// this callback handler is called every time a buffer finishes playing
void bqPlayerCallback(SLAndroidSimpleBufferQueueItf bq, void *context)
{
    //获取 PCM 采样数据 , 将重采样的数据放到 data 中
    int data_size ; 
	//进行 FFMPEG 音频重采样 ... 大块代码参考上一篇博客 
    //开始播放
    if ( data_size > 0 ){
        //通过播放器队列接口 , 将 PCM 数据加入到该队列缓冲区后 , 就会自动播放这段音频
        //  注意 , 最后一个参数是样本字节数
        (*bq)->Enqueue(bq, audioChannel->data, data_size);
    }
}

【Android FFMPEG 开发】OpenSLES 播放音频 ( 创建引擎 | 输出混音设置 | 配置输入输出 | 创建播放器 | 获取播放/队列接口 | 回调函数 | 开始播放 | 激活回调 )



XII . FFMPEG 音视频同步

1 . 音视频同步总结 :

以音频播放的时间为基准 , 调整视频的播放速度 , 让视频与音频进行同步 ;

先计算出音频的播放时间 ; 再计算视频的播放时间 ;

根据视频与音频之间的比较 , 如果视频比音频快 , 那么增大视频帧之间的间隔 , 降低视频帧绘制速度 ;
如果视频比音频慢 , 那么需要丢弃部分视频帧 , 以追赶上音频的速度 ;



2 . 音视频同步代码示例 :

//根据帧率 ( fps ) 计算两次图像绘制之间的间隔
//  注意单位换算 : 实际使用的是微秒单位 , 使用 av_usleep ( ) 方法时 , 需要传入微秒单位 , 后面需要乘以 10 万
double frame_delay = 1.0 / fps;
while (isPlaying){
    //从线程安全队列中获取 AVFrame * 图像
    ...
    //获取当前画面的相对播放时间 , 相对 : 即从播放开始到现在的时间
    //  该值大多数情况下 , 与 pts 值是相同的
    //  该值比 pts 更加精准 , 参考了更多的信息
    //  转换成秒 : 这里要注意 pts 需要转成 秒 , 需要乘以 time_base 时间单位
    //  其中 av_q2d 是将 AVRational 转为 double 类型
    double vedio_best_effort_timestamp_second = avFrame->best_effort_timestamp * av_q2d(time_base);
    //解码时 , 该值表示画面需要延迟多长时间在显示
    //  extra_delay = repeat_pict / (2*fps)
    //  需要使用该值 , 计算一个额外的延迟时间
    //  这里按照文档中的注释 , 计算一个额外延迟时间
    double extra_delay = avFrame->repeat_pict / ( fps * 2 );
    //计算总的帧间隔时间 , 这是真实的间隔时间
    double total_frame_delay = frame_delay + extra_delay;
    //将 total_frame_delay ( 单位 : 秒 ) , 转换成 微秒值 , 乘以 10 万
    unsigned microseconds_total_frame_delay = total_frame_delay * 1000 * 1000;
    if(vedio_best_effort_timestamp_second == 0 ){
        //如果播放的是第一帧 , 或者当前音频没有播放 , 就要正常播放
        //休眠 , 单位微秒 , 控制 FPS 帧率
        av_usleep(microseconds_total_frame_delay);
    }else{
        //如果不是第一帧 , 要开始考虑音视频同步问题了
        //获取音频的相对时间
        if(audioChannel != NULL) {
            //音频的相对播放时间 , 这个是相对于播放开始的相对播放时间
            double audio_pts_second = audioChannel->audio_pts_second;
            //使用视频相对时间 - 音频相对时间
            double second_delta = vedio_best_effort_timestamp_second - audio_pts_second;
            //将相对时间转为 微秒单位
            unsigned microseconds_delta = second_delta * 1000 * 1000;
            //如果 second_delta 大于 0 , 说明视频播放时间比较长 , 视频比音频快
            //如果 second_delta 小于 0 , 说明视频播放时间比较短 , 视频比音频慢
            if(second_delta > 0){
                //视频快处理方案 : 增加休眠时间
                //休眠 , 单位微秒 , 控制 FPS 帧率
                av_usleep(microseconds_total_frame_delay + microseconds_delta);
            }else if(second_delta = 0.05){
                    //丢弃解码后的视频帧
                    ...
                    //终止本次循环 , 继续下一次视频帧绘制
                    continue;
if
                }else{
                    //如果音视频之间差距低于 0.05 秒 , 不操作 ( 50ms )
                }
            }
        }
    }

【Android FFMPEG 开发】FFMPEG 音视频同步 ( 音视频同步方案 | 视频帧 FPS 控制 | H.264 编码 I / P / B 帧 | PTS | 音视频同步 )



XIII . GitHub 代码地址

1 . GitHub 代码地址 : FFMPEG 直播示例

2 . 效果展示 : 使用的是湖南卫视的直播源 rtmp://58.200.131.2:1935/livetv/hunantv
在这里插入图片描述


作者:韩曙亮


免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

【Android FFMPEG 开发】FFMPEG 直播功能完整流程 + 源码 ( 源码交叉编译 -&gt; AS工程配置 -&gt; 音视频打开/读取/解码/格式转换 -&

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

【Android FFMPEG 开发】FFMPEG 直播功能完整流程 + 源码 ( 源码交叉编译 -&gt; AS工程配置 -&gt; 音视频打开/读取/解码/格式转换 -&

文章目录I . FFMPEG 播放视频流程总结II . FFMPEG 下载及交叉编译III . Android Studio 配置 FFMPEG 函数库IV . FFMPEG 初始化V . FFMPEG 获取 AVStream 音视频流VI
2022-06-06

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录