开编
之前在Android集成FFmpeg。主要还是基于命令行的方式进行操作。刚刚好最近又在研究推流相关的东西。看了一些博文。和做了一些实践。
就希望通过本文记录袭来。
本文的大体结构如下
PC端
FFMPEG 开发环境搭建
笔者是在 Windows10 64+Visual Studio2017的环境下开发的
下载和安装VisualStudio2017
去官网下载和安装就可以
在项目中配置FFMPEG
- 下载FFMPEG相关的文件和解压
从FFMPEG WINDOW BUILD中下载dev
和shared
两个部分的内容
-
dev
压缩包内
-
shared
压缩包内
- 创建VisualStudio项目和配置FFMPEG
-
创建控制台项目
在项目中配置依赖项(重点)
-
在左上角,点击项目。最后一下的弹出框中进行配置。
-
然后将dll的文件复制到当前的目录下。
-
将Window编译调试,选择到正确的x64
处理一些错误。让程序跑起来
错误1:
av_register_all
过时。
解决方法: 暂时没有什么更好的办法,只能去头文件里面。把attribute_deprecated
注释掉了
推流代码
大致先了解一下结构体和结构体之间的关系
结构体关系
结构体
-
AVFormatContext
AVFormatContext
是格式封装的上下文对象。
在这里,会比较熟悉的常用的成员变量有:-
AVIOContext *pb
:用来合成音频和视频,或者分解的AVIOContext -
unsigned int nb_streams
:视音频流的个数 -
AVStream **streams
:视音频流 -
char filename[1024]
:文件名 -
AVDictionary *metadata
:存储视频元信息的metadata对象。
-
AVDictionaryEntry
每一条元数据分为key
和value
两个属性。
typedef struct AVDictionaryEntry {
char *key;
char *value;
} AVDictionaryEntry;
可以根据下面代码。取出这些数据
AVFormatContext *fmt_ctx = NULL;
AVDictionaryEntry *tag = NULL;
int ret;
if ((ret = avformat_open_input(&fmt_ctx, argv[1], NULL, NULL)))
return ret;
while ((tag = av_dict_get(fmt_ctx->metadata, "", tag, AV_DICT_IGNORE_SUFFIX)))
printf("%s=%s\n", tag->key, tag->value);
avformat_close_input(&fmt_ctx);
-
AVRational
表示媒体信息的一些分数,是分母和分子的结构。计算过程中,会多次使用这样的数据结构
typedef struct AVRational{
int num; ///< Numerator
int den; ///< Denominator
} AVRational;
-
AVPacket
AVPacket
是存储压缩编码数据相关信息的结构体。-
uint8_t *data
:压缩编码的数据。
例如对于H.264来说。1个AVPacket的data通常对应一个NAL!
注意:在这里只是对应,而不是一模一样。他们之间有微小的差别:使用FFMPEG类库分离出多媒体文件中的H.264码流
因此在使用FFMPEG进行视音频处理的时候,常常可以将得到的AVPacket的data数据直接写成文件,从而得到视音频的码流文件。
-int size
:
data的大小 -
int64_t pts
:
显示时间戳
-int64_t dts
:
解码时间戳
-int stream_index
:
标识该AVPacket所属的视频/音频流。
-
FFMPEG推流的套路
套路图如下:
整个方法的流向:
首先,我们先来熟悉一下这个整体的套路。其实推流的过程。我的理解是,经过解封装,按照原来的数据结构,提取和转成目标数据结构进行发送。
因为FFmpeg做好了封装,我们只要对其调用方法就可以了。
按照套路图,我们知道,使用FFmpeg的话
- 第一步是得到整体封装的输入和输出的上下文对象
AVFormatContext
。
//注册所有的
av_register_all();
//初始化网络
avformat_network_init();
//配置输入和输出
const char *inUrl = "dongfengpo.flv";
const char *outUrl = "rtmp://localhost/live/test";
AVFormatContext *ictx = NULL;
//得到输入的上下文
int ret = avformat_open_input(&ictx, inUrl, NULL, NULL);
if (ret < 0)
{
return avError2(ret);
}
cout << " avformat_open_input success! " << endl;
//去打印结果
ret = avformat_find_stream_info(ictx, NULL);
if (ret < 0)
{
return avError2(ret);
}
//将AVFormat打印出来
av_dump_format(ictx, 0, inUrl, 0);
//开始处理输出流
int videoIndex = 0;
//0.先得到AVFormat
AVFormatContext *octx;
AVOutputFormat *ofmt = NULL;
ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
if (ret < 0)
{
return avError2(ret);
}
cout << "avformat_alloc_output_context2 success!" << endl;
ofmt = octx->oformat;
- 再创建输出的
AVStream
,并从输入AVFormatContext
的其中取得AVStream
,将对应的参数(主要是编码器信息)copy
到其中。
//开始遍历流,进行对应stream的创建
for (int i = 0; i < ictx->nb_streams; i++)
{
//这里开始要创建一个新的AVStream
AVStream *stream = ictx->streams[i];
//判断是否是videoIndex。这里先记录下视频流。后面会对这个流进行操作
if (stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO)
{
videoIndex = i;
}
//创建输出流
AVCodec *c = avcodec_find_decoder(stream->codecpar->codec_id);
AVStream *os = avformat_new_stream(octx, c);
//应该将编解码器的参数从input中复制过来
// 这里要注意的是,因为 os->codec这样的取法,已经过时了。所以使用codecpar
ret = avcodec_parameters_copy(os->codecpar, stream->codecpar);
if (ret < 0)
{
return avError2(ret);
}
cout << "avcodec_parameters_copy success!" << endl;
cout << "avcodec_parameters_copy success! in stream codec tag" << stream->codecpar->codec_tag << endl;
cout << "avcodec_parameters_copy success! out stream codec tag" << os->codecpar->codec_tag << endl;
//复制成功之后。还需要设置 codec_tag(编码器的信息?)
os->codecpar->codec_tag = 0;
}
//检查一遍我们的输出
av_dump_format(octx, 0, outUrl, 1);
- 因为是推流,所以第三部,就是通过
avio_open
链接网址,做好推流的准备
//开始使用io进行推流
//通过AVIO_FLAG_WRITE这个标记位,打开输出的AVFormatContext->AVIOContext
ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
if (ret < 0)
{
return avError2(ret);
}
cout << "avio_open success!" << endl;
- 推流的过程。首先通过
avformat_write_header
写入头部信息。接着是通过av_read_frame
函数读取输入的frame
的数据,写入到AVPakcet
当中。处理每一帧的pts
和dts
。再通过av_interleaved_write_frame
将这一个帧发送出去。最后,通过av_packet_unref
释放AVPacket
//先写头
ret = avformat_write_header(octx, 0);
if (ret < 0)
{
return avError2(ret);
}
//取得到每一帧的数据,写入
AVPacket pkt;
//为了让我们的代码发送流的速度,相当于整个视频播放的数据。需要记录程序开始的时间
//后面再根据,每一帧的时间。做适当的延迟,防止我们的代码发送的太快了
long long start_time = av_gettime();
//记录视频帧的index,用来计算pts
long long frame_index = 0;
while (true)
{
//输入输出视频流
AVStream *in_stream, *out_stream;
//从输入流中读取数据 frame到AVPacket当中
ret = av_read_frame(ictx, &pkt);
if (ret < 0)
{
break;
}
//没有显示时间的时候,才会进入计算和校验
//没有封装格式的裸流(例如H.264裸流)是不包含PTS、DTS这些参数的。在发送这种数据的时候,需要自己计算并写入AVPacket的PTS,DTS,duration等参数。如果没有pts,则进行计算
if (pkt.pts == AV_NOPTS_VALUE)
{
//AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。
//先得到流中的time_base
AVRational time_base = ictx->streams[videoIndex]->time_base;
//开始校对pts和 dts.通过time_base和dts转成真正的时间
//得到的是每一帧的时间
/*
r_frame_rate 基流帧速率 。取得是时间戳内最小的帧的速率 。每一帧的时间就是等于 time_base/r_frame_rate
av_q2d 转化为double类型
*/
int64_t calc_duration = (double)AV_TIME_BASE / av_q2d(ictx->streams[videoIndex]->r_frame_rate);
//配置参数 这些时间,都是通过 av_q2d(time_base) * AV_TIME_BASE 来转成实际的参数
pkt.pts = (double)(frame_index * calc_duration) / (double)av_q2d(time_base) * AV_TIME_BASE;
//一个GOP中,如果存在B帧的话,只有I帧的dts就不等于pts
pkt.dts = pkt.pts;
pkt.duration = (double)calc_duration / (double)av_q2d(time_base) * AV_TIME_BASE;
}
//开始处理延迟.只有等于视频的帧,才会处理
if (pkt.stream_index == videoIndex)
{
//需要计算当前处理的时间和开始处理时间之间的间隔??
//0.先取时间基数
AVRational time_base = ictx->streams[videoIndex]->time_base;
//AV_TIME_BASE_Q 用小数表示的时间基数。等于时间基数的倒数
AVRational time_base_r = { 1, AV_TIME_BASE };
//计算视频播放的时间. 公式等于 pkt.dts * time_base / time_base_r`
//.其实就是 stream中的time_base和定义的time_base直接的比例
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_r);
//计算实际视频的播放时间。 视频实际播放的时间=代码处理的时间??
int64_t now_time = av_gettime() - start_time;
cout << time_base.num << " " << time_base.den << " " << pkt.dts << " " << pkt.pts << " " << pts_time << endl;
//如果显示的pts time 比当前的时间迟,就需要手动让程序睡一会,再发送出去,保持当前的发送时间和pts相同
if (pts_time > now_time)
{
//睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
av_usleep((unsigned int)(pts_time - now_time));
}
}
//重新计算一次pts和dts.主要是通过 in_s的time_base 和 out_s的time_base进行计算和校对
//先取得stream
in_stream = ictx->streams[pkt.stream_index];
out_stream = octx->streams[pkt.stream_index];
//重新开始指定时间戳
//计算延时后,重新指定时间戳。 这次是根据 in_stream 和 output_stream之间的比例
//计算dts时,不再直接用pts,因为如有有B帧,就会不同
//pts,dts,duration都也相同
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.duration = (int)av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
//再次标记字节流的位置,-1表示不知道字节流的位置
pkt.pos = -1;
//如果当前的帧是视频帧,则将我们定义的frame_index往后推
if (pkt.stream_index == videoIndex)
{
printf("Send %8d video frames to output URL\n", frame_index);
frame_index++;
}
//发送!!!
ret = av_interleaved_write_frame(octx, &pkt);
if (ret < 0)
{
printf("发送数据包出错\n");
break;
}
//使用完了,记得释放
av_packet_unref(&pkt);
}
//写文件尾(Write file trailer)
av_write_trailer(octx);
avformat_close_input(&ictx);
/* close output */
if (ictx && !(octx->flags & AVFMT_NOFILE))
avio_close(octx->pb);
avformat_free_context(octx);
if (ret < 0 && ret != AVERROR_EOF) {
printf("Error occurred.\n");
return -1;
}
Android端
基本的代码逻辑和上面是一致的。只是需要集成到Android当中。
集成
-
将编译好的ffmpeg复制到libs下
- 编写Cmake文件
将ffmpeg加入链接库。同时将include的文件路径设置正确
cmake_minimum_required(VERSION 3.4.1)
set(INC_DIR ${CMAKE_SOURCE_DIR}/libs/include)
set(LINK_DIR ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI})
include_directories(${INC_DIR})
add_library(ffmpeg SHARED IMPORTED)
set_target_properties(ffmpeg PROPERTIES IMPORTED_LOCATION ${LINK_DIR}/libffmpeg.so)
add_library(native-lib
SHARED
src/main/cpp/ffmpeg_push.cpp )
find_library(log-lib
log )
target_link_libraries( native-lib
ffmpeg
${log-lib} )
-
gradle
修改gradle文件
- 配置abiFilter
因为我们只编译了这一种so文件 - 配置jniLibs
需要把libffmpeg.so的文件配置成jniLibs的目标
代码
ffmpeg_push.cpp
代码的整体流程和思路和PC端一致。
#include <jni.h>
#include <string>
#include<android/log.h>
#include <exception>
//定义日志宏变量
#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"ZZX",FORMAT,##__VA_ARGS__);
//#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"ZZX",FORMAT,##__VA_ARGS__);
#define logw(content) __android_log_write(ANDROID_LOG_WARN,"eric",content)
#define loge(content) __android_log_write(ANDROID_LOG_ERROR,"eric",content)
#define logd(content) __android_log_write(ANDROID_LOG_DEBUG,"eric",content)
extern "C" {
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
//引入时间
#include "libavutil/time.h"
}
#include <iostream>
using namespace std;
int avError(int errNum) {
char buf[1024];
//获取错误信息
av_strerror(errNum, buf, sizeof(buf));
loge(string().append("发生异常:").append(buf).c_str());
return -1;
}
extern "C"
JNIEXPORT jstring JNICALL
Java_com_cry_ffmpegpushdemo_FFpush_getAvFormatInfo(JNIEnv *env, jobject instance) {
char info[40000] = {0};
av_register_all();
AVInputFormat *in_temp = av_iformat_next(NULL);
AVOutputFormat *out_temp = av_oformat_next(NULL);
while (in_temp != NULL) {
sprintf(info, " %s , Input: %s\n",info, in_temp->name);
// logd("Input =", in_temp->name);
in_temp = in_temp->next;
}
while (out_temp != NULL) {
sprintf(info, "%s ,Output: %s\n", info,out_temp->name);
// logd("Output =", out_temp->name);
out_temp = out_temp->next;
}
return env->NewStringUTF(info);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_cry_ffmpegpushdemo_FFpush_push(JNIEnv *env, jobject instance, jstring fileName_,
jstring pushUrl_) {
const char *fileName = env->GetStringUTFChars(fileName_, 0);
const char *pushUrl = env->GetStringUTFChars(pushUrl_, 0);
int videoindex = -1;
//所有代码执行之前要调用av_register_all和avformat_network_init
//初始化所有的封装和解封装 flv mp4 mp3 mov。不包含编码和解码
av_register_all();
//初始化网络库
avformat_network_init();
const char *inUrl = fileName;
//输出的地址
const char *outUrl = pushUrl;
//////////////////////////////////////////////////////////////////
// 输入流处理部分
/////////////////////////////////////////////////////////////////
//打开文件,解封装 avformat_open_input
//AVFormatContext **ps 输入封装的上下文。包含所有的格式内容和所有的IO。如果是文件就是文件IO,网络就对应网络IO
//const char *url 路径
//AVInputFormt * fmt 封装器
//AVDictionary ** options 参数设置
AVFormatContext *ictx = NULL;
AVFormatContext *octx = NULL;
AVPacket pkt;
int ret = 0;
try {
//打开文件,解封文件头
ret = avformat_open_input(&ictx, inUrl, 0, NULL);
if (ret < 0) {
avError(ret);
throw ret;
}
logd("avformat_open_input success!");
//获取音频视频的信息 .h264 flv 没有头信息
ret = avformat_find_stream_info(ictx, 0);
if (ret != 0) {
avError(ret);
throw ret;
}
//打印视频视频信息
//0打印所有 inUrl 打印时候显示,
av_dump_format(ictx, 0, inUrl, 0);
//////////////////////////////////////////////////////////////////
// 输出流处理部分
/////////////////////////////////////////////////////////////////
//如果是输入文件 flv可以不传,可以从文件中判断。如果是流则必须传
//创建输出上下文
ret = avformat_alloc_output_context2(&octx, NULL, "flv", outUrl);
if (ret < 0) {
avError(ret);
throw ret;
}
logd("avformat_alloc_output_context2 success!");
int i;
for (i = 0; i < ictx->nb_streams; i++) {
//获取输入视频流
AVStream *in_stream = ictx->streams[i];
//为输出上下文添加音视频流(初始化一个音视频流容器)
AVStream *out_stream = avformat_new_stream(octx, in_stream->codec->codec);
if (!out_stream) {
printf("未能成功添加音视频流\n");
ret = AVERROR_UNKNOWN;
}
if (octx->oformat->flags & AVFMT_GLOBALHEADER) {
out_stream->codec->flags |= CODEC_FLAG_GLOBAL_HEADER;
}
ret = avcodec_parameters_copy(out_stream->codecpar, in_stream->codecpar);
if (ret < 0) {
printf("copy 编解码器上下文失败\n");
}
out_stream->codecpar->codec_tag = 0;
// out_stream->codec->codec_tag = 0;
}
//找到视频流的位置
for (i = 0; i < ictx->nb_streams; i++) {
if (ictx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) {
videoindex = i;
break;
}
}
av_dump_format(octx, 0, outUrl, 1);
//////////////////////////////////////////////////////////////////
// 准备推流
/////////////////////////////////////////////////////////////////
//打开IO
ret = avio_open(&octx->pb, outUrl, AVIO_FLAG_WRITE);
if (ret < 0) {
avError(ret);
throw ret;
}
logd("avio_open success!");
//写入头部信息
ret = avformat_write_header(octx, 0);
if (ret < 0) {
avError(ret);
throw ret;
}
logd("avformat_write_header Success!");
//推流每一帧数据
//int64_t pts [ pts*(num/den) 第几秒显示]
//int64_t dts 解码时间 [P帧(相对于上一帧的变化) I帧(关键帧,完整的数据) B帧(上一帧和下一帧的变化)] 有了B帧压缩率更高。
//获取当前的时间戳 微妙
long long start_time = av_gettime();
long long frame_index = 0;
logd("start push >>>>>>>>>>>>>>>");
while (1) {
//输入输出视频流
AVStream *in_stream, *out_stream;
//获取解码前数据
ret = av_read_frame(ictx, &pkt);
if (ret < 0) {
break;
}
/*
PTS(Presentation Time Stamp)显示播放时间
DTS(Decoding Time Stamp)解码时间
*/
//没有显示时间(比如未解码的 H.264 )
if (pkt.pts == AV_NOPTS_VALUE) {
//AVRational time_base:时基。通过该值可以把PTS,DTS转化为真正的时间。
AVRational time_base1 = ictx->streams[videoindex]->time_base;
//计算两帧之间的时间
/*
r_frame_rate 基流帧速率 (不是太懂)
av_q2d 转化为double类型
*/
int64_t calc_duration =
(double) AV_TIME_BASE / av_q2d(ictx->streams[videoindex]->r_frame_rate);
//配置参数
pkt.pts = (double) (frame_index * calc_duration) /
(double) (av_q2d(time_base1) * AV_TIME_BASE);
pkt.dts = pkt.pts;
pkt.duration =
(double) calc_duration / (double) (av_q2d(time_base1) * AV_TIME_BASE);
}
//延时
if (pkt.stream_index == videoindex) {
AVRational time_base = ictx->streams[videoindex]->time_base;
AVRational time_base_q = {1, AV_TIME_BASE};
//计算视频播放时间
int64_t pts_time = av_rescale_q(pkt.dts, time_base, time_base_q);
//计算实际视频的播放时间
int64_t now_time = av_gettime() - start_time;
AVRational avr = ictx->streams[videoindex]->time_base;
cout << avr.num << " " << avr.den << " " << pkt.dts << " " << pkt.pts << " "
<< pts_time << endl;
if (pts_time > now_time) {
//睡眠一段时间(目的是让当前视频记录的播放时间与实际时间同步)
av_usleep((unsigned int) (pts_time - now_time));
}
}
in_stream = ictx->streams[pkt.stream_index];
out_stream = octx->streams[pkt.stream_index];
//计算延时后,重新指定时间戳
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base,
(AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base,
(AVRounding) (AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.duration = (int) av_rescale_q(pkt.duration, in_stream->time_base,
out_stream->time_base);
// __android_log_print(ANDROID_LOG_WARN, "eric", "duration %d", pkt.duration);
//字节流的位置,-1 表示不知道字节流位置
pkt.pos = -1;
if (pkt.stream_index == videoindex) {
LOGI("Send %d video frames to output URL\n", frame_index);
frame_index++;
}
//回调数据
// callback(env, pkt.pts, pkt.dts, pkt.duration, frame_index);
//向输出上下文发送(向地址推送)
ret = av_interleaved_write_frame(octx, &pkt);
if (ret < 0) {
printf("发送数据包出错\n");
break;
}
//释放
av_packet_unref(&pkt);
}
ret = 0;
} catch (int errNum) {
}
logd("finish===============");
//关闭输出上下文,这个很关键。
if (octx != NULL)
avio_close(octx->pb);
//释放输出封装上下文
if (octx != NULL)
avformat_free_context(octx);
//关闭输入上下文
if (ictx != NULL)
avformat_close_input(&ictx);
octx = NULL;
ictx = NULL;
env->ReleaseStringUTFChars(fileName_, fileName);
env->ReleaseStringUTFChars(pushUrl_, pushUrl);
}
参考
基于FFmpeg进行RTMP推流
最简单的基于FFmpeg的推流器(以推送RTMP为例)
FFMPEG中最关键的结构体之间的关系
FFMPEG结构体分析:AVPacket