【Android FFMPEG 开发】FFMPEG 直播功能完整流程 + 源码 ( 源码交叉编译 -> AS工程配置 -> 音视频打开/读取/解码/格式转换 -&
本博客属于总结性质的博客 , 开发时可以参考该博客的代码示例 , 可以直接使用 ; 知识点比较概括 , 只描述操作流程及核心源码 , 没有具体原理说明 , 详细的原理知识去具体的单条知识点博客中查看 ;
代码及播放效果 :
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