服务器用的是ngix+rtmp,配置方式见博客:
https://blog.csdn.net/goldfish3/article/details/100036059
代码涉及一些ffmpeg的知识点,这里列一下:
h264帧中,B帧需要前面的帧和后面的帧,才能解码,因此解码时间和播放时间会不一样,比如一段帧:
按播放顺序为:I B B P
但由于B帧解码需要P帧,因此解码顺序为:I P B B
因此,解码序列和播放序列就需要使用不同的时间戳来表示~
PTS:显示时间戳,对应的数据格式为 AVFrame(解码后的帧),通过 AVCodecContext->time_base 获取。
DTS:解码时间戳,对应的数据格式为AVPacket(解码前的包),通过AVStream->time_base获取。
而ffmpeg中,时间戳并不是以自然时间表示的,而是以一个个格子的形式,不同的格子代表不同的 time base。格子本身使用结构体 AVRational表示:
typedef struct AVRational{
int num; //分子
int den; //分母
} AVRational;
比如num=1,den=200,一个格子就表示 1/200 秒,PTS和DTS中的时间戳都是用 AVRational表示的。
我们应该在什么时间,将包推给客户端?下面的代码中选用的是包解码的时间,也就是DTS时间,因此,如果当前要推的包还没有到解码时间,就需要等一会儿再推。真实环境下,由于网络问题等,处理会更更复杂一些。
下面代码涉及的api
AVFormatContext():c语言不是面向对象的,因此所有参数和对象需要使用一个上下文进行传递和管理,ffmpeg里就是它了。
avformat_open_input():通过路径,将视频信息读入上下文。
avformat_find_stream_info():获取流信息。
av_dump_format():获取视频封装信息,比如视频中有哪些流,流的具体情况。
avformat_network_init():网络初始化
avformat_alloc_output_context2():新建输出上下文
avformat_new_stream():新建流,并将流放入上下文
avcodec_parameters_copy():拷贝流参数
avio_open():打开流,在这里输出流是网络,应该就是作网络推流的一些准备工作和参数设置
avformat_write_header():写入头信息(也不知掉这些头信息是从哪来的)
av_read_frame():虽然叫 read frame,但是这里实际上读取的是一个包(AVPacket),也就是解码前的数据。
av_q2d():将AVRational转化为秒
av_interleaved_write_frame():将数据排序,写入
总的流程大概就是:
获取输出上下文 -> 建立输出上下文 -> 复制流参数信息到输出上下文 -> 使用输出上下文打开输出流 -> 从输入流中读取数据,使用输出流推出去。
#include
#include "string.h"
extern "C"{
#include
#include
#include
#include
#include "libavutil/time.h"
#include
}
using namespace std;
//
#include
#define __STDC_CONSTANT_MACROS
#define USE_H264BSF 0
void printErrWithCode(int errCode){
char buf[1024] = {0};
av_strerror(errCode, buf, sizeof(buf));
cout << buf << endl;
return;
}
int main(){
AVFormatContext *ictx = NULL;
AVFormatContext *octx = NULL;
string in_path = "/Users/heyutang/Desktop/test.flv";
string out_path = "rtmp://127.0.0.1:1935/rtmplive/room";
int ret = 0;
avformat_network_init();
//打开文件,解封装协议头
//类似于mp4都有文件头,文件头里放了所有的文件信息。
ret = avformat_open_input(&ictx, in_path.c_str(), 0, 0);
if (ret) {
printErrWithCode(ret);
}
ret = avformat_find_stream_info(ictx, NULL);
av_dump_format(ictx, 0, in_path.c_str(), false);
//第二个参数是输出格式
ret = avformat_alloc_output_context2(&octx, NULL, "flv", out_path.c_str());
if (!octx) {
printErrWithCode(ret);
}
//遍历所有输出流
for (int i=0; i<ictx->nb_streams; i++) {
//创建一个输出流
AVStream *out = avformat_new_stream(octx, ictx->streams[i]->codec->codec);
if (!out) {
ret = 0;
printErrWithCode(ret);
}
//将输入流的配置信息(AVCodecContext)复制到输出流中
//上面这种是老的方式,下面这种是新的,但是对mp4文件可能会有问题
// avcodec_copy_context(out->codec, ictx->streams[i]->codec);
avcodec_parameters_copy(out->codecpar, ictx->streams[i]->codecpar);
out->codec->codec_tag = 0;
}
av_dump_format(octx, 0, out_path.c_str(), true);
//打开流,写入头信息,至此,输出上下文建立完成
ret = avio_open(&octx->pb, out_path.c_str(), AVIO_FLAG_WRITE);
if (!octx->pb) {
printErrWithCode(ret);
}
ret = avformat_write_header(octx, 0);
if (ret < 0) {
printErrWithCode(ret);
}
//推流每一帧数据
AVPacket pkt;
long long startTime = av_gettime(); //获取当前的时间戳(微秒)
while (1) {
ret = av_read_frame(ictx, &pkt); //这一步虽然名字叫 read_frame,但实际上读取出来的是 packet
if (ret) {
printErrWithCode(ret);
}
//计算转换 pts dts(time base可能不同)(但是实际上,段点调试发现time_base是相同的,不知道是哪一步复制的time_base)
// AVRational itime = ictx->streams[pkt.stream_index]->time_base;
// AVRational otime = octx->streams[pkt.stream_index]->time_base;
// pkt.pts = av_rescale_q_rnd(pkt.pts, itime, otime,
// (AVRounding)(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
// pkt.dts = av_rescale_q_rnd(pkt.dts, itime,otime,
// (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
// pkt.duration = av_rescale_q_rnd(pkt.duration, itime, otime,
// (AVRounding)(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX));
// pkt.pos = -1;
//这里假设网络传输是没问题的,那么我们应该在dts也就是解码时间进行推流。
//如果推流时间早于解码时间,那么就等一会儿。
if (ictx->streams[pkt.stream_index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
AVRational tb = ictx->streams[pkt.stream_index]->time_base;
long long now = av_gettime() - startTime;
long long dts = pkt.dts * (1000 * 1000 * av_q2d(tb)); //将秒转化为微秒
if (dts > now) {
av_usleep(dts - now);
}
}
//根据pts dts进行排序,然后写入(由于是网络流,这一段就是网络传输)
ret = av_interleaved_write_frame(octx, &pkt);
if (ret < 0) {
printErrWithCode(ret);
}
av_packet_unref(&pkt);
}
}