工作需要开始接触流媒体内容,留个脚印;
流媒体转发服务中需要设计一个模块实现接收HLS网络流,解析出H264、aac格式音视频裸流发送到到AppServer模块转成rtmp\rtsp等流发送出去。这就需要先接收HLS网络流解HLS协议获取ts流,然后解复用ts流获取h264\aac。实际中HLS网络了的.m3u8文件中参数有差异,通过查找目前常见支持HLS流解析的有ffmpeg和VLC,VLC源码移植困难,选择ffmpeg库。
ffmpeg中支持HLS解协议部分参见博客:
附:两幅前辈大神梳理的ffmpeg函数调用关系图
解析代码:
调用ffmpeg库做HLS解协议不必纠结.m3u8内部不同,ffmpeg会自己探测,我们只需把它当作普通的rtsp流接收就行得到ts流。
void TsDemuxer::DemuxerCycle() {
while (GetRunStatus()) {
int ret = 0;
ifmt_ctx = NULL, ofmt_ctx = NULL;
pb = NULL;
ofmt = (AVOutputFormat*)av_mallocz(sizeof(AVOutputFormat));//输出格式
int nVideoFramesNum = 0;
uint8_t* buf = (uint8_t*)av_mallocz(sizeof(uint8_t)* 32768);
int width = 0;
int height = 0;
int codeType = 0;
int fpsDen = 0;
int fpsNum = 0;
video_index = -1;
av_register_all();
avformat_network_init();
//打开输入流
if ((ret = avformat_open_input(&ifmt_ctx, m_strFilePath.c_str(), 0, 0)) < 0)
{
log_error("Could not open input stream");
break;
}
if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0)
{
log_error("Failed to retrieve input stream information");
break;
}
//nb_streams代表有几路流,一般是2路:即音频和视频,顺序不一定
for(int i=0;i<ifmt_ctx->nb_streams;i++){
if(ifmt_ctx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO)
{
//这一路是视频流,标记一下,以后取视频流都从ifmt_ctx->streams[video_index]取
video_index=i;
break;
}
}
av_dump_format(ifmt_ctx, 0, m_strFilePath.c_str(), 0);
pb = avio_alloc_context(buf, 188, 1, this, NULL, WritePacket, NULL);
avformat_alloc_output_context2(&ofmt_ctx, NULL, "mpegts", NULL/*out_filename*/);//设置输出视频的格式为TS格式,则设置为"mpegts"
if (!ofmt_ctx) {
log_error("Could not create output context\n");
ret = AVERROR_UNKNOWN;
goto end;
}
ofmt_ctx->pb = pb;//这个是关键,指定输出的方式
ofmt_ctx->flags |= AVFMT_FLAG_CUSTOM_IO;
log_debug("output format:%s[%s]\n", ofmt_ctx->oformat->name, ofmt_ctx->oformat->long_name);
if(!ofmt_ctx)
{
log_error("Could not create output context\n");
ret=AVERROR_UNKNOWN;
goto end;
}
ofmt = ofmt_ctx->oformat;
for(int i=0;i<ifmt_ctx->nb_streams;i++)
{ //根据输入流创建输出流
AVStream *in_stream = ifmt_ctx->streams[i];
AVStream *out_stream = avformat_new_stream(ofmt_ctx,in_stream->codec->codec);
if(!out_stream)
{
log_error("Failed allocating output stream.\n");
ret = AVERROR_UNKNOWN;
goto end;
}
//将输出流的编码信息复制到输入流
ret = avcodec_copy_context(out_stream->codec,in_stream->codec);
if(ret<0)
{
log_error("Failed to copy context from input to output stream codec context\n");
goto end;
}
out_stream->codec->codec_tag = 0;
if(ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
out_stream->codec->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
}
av_dump_format(ofmt_ctx,0,NULL,1);
ofmt_ctx->streams[0]->codec->codec_type = AVMEDIA_TYPE_VIDEO;
ofmt_ctx->streams[0]->codec->width = ifmt_ctx->streams[video_index]->codec->width;
ofmt_ctx->streams[0]->codec->height = ifmt_ctx->streams[video_index]->codec->height;
ofmt_ctx->streams[0]->codec->time_base.num = ifmt_ctx->streams[video_index]->codec->time_base.num;
ofmt_ctx->streams[0]->codec->time_base.den = ifmt_ctx->streams[video_index]->codec->time_base.den;
ofmt_ctx->streams[0]->codec->codec_id = AV_CODEC_ID_H264;
ret = avformat_write_header(ofmt_ctx,NULL);
if(ret < 0)
{
log_error("Error occured when opening output URL\n");
goto end;
}
//实现REMUXING
while (1) {
AVStream *in_stream, *out_stream;
ret = av_read_frame(ifmt_ctx, &pkt);
if (ret < 0)
break;
in_stream = ifmt_ctx->streams[pkt.stream_index];
out_stream = ofmt_ctx->streams[pkt.stream_index];
if(in_stream->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
if(pkt.pts == AV_NOPTS_VALUE) //FIX:No PTS (Example: Raw H.264)
{
AVRational time_base1 = out_stream->time_base;
//Duration between 2 frames (us)
int64_t calc_duration =(double)AV_TIME_BASE/av_q2d(in_stream->r_frame_rate);
pkt.pts = (double)(nVideoFramesNum*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);
}
else
{
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 = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
}
nVideoFramesNum++;
}
else
{
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 = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);
pkt.pos = -1;
}
ret = av_interleaved_write_frame(ofmt_ctx, &pkt);
if (ret < 0) {
log_error("Error muxing packet\n");
break;
}
av_free_packet(&pkt);
}
av_write_trailer(ofmt_ctx);
end:
break;
}
}
}
上面代码由于本人接收的设备只有视频流,所以这里去掉了音频;
这里最重要的一个函数是:
pb = avio_alloc_context(buf, 188, 1, this, NULL, WritePacket, NULL);
WritePacket是个结果回调函数,得到的就是ts流,第三个参数设置为1进入回调,第二个参数为是数据缓冲空间,满多少字节进入一次回调,这里设置为188是为ts文件解复用所用,ts解复用协议中规定了单次输入字节为188,不然回调没作用。
输出ts流的回调函数WritePacket:
int WritePacket(void *opaque, uint8_t *buf, int buf_size){
TsDemuxer *p = (TsDemuxer*) opaque;
size_t ret = ts_demuxer_input((ts_demuxer_t*)p->ts, buf, buf_size);
if (ret != 0 ){
log_error("ts demuxer input error");
}
return 0;
}
ts文件解复用:
size_t ret = ts_demuxer_input((ts_demuxer_t*)p->ts, buf, buf_size);
这个函数就是ts文件解复用的输入,同样输出的h264,acc是通过回调函数形式,这里需要注意的是,如果需要ts解复用就需要在所有函数之前创建上下文,本人把创建上下文函数放在了类的构造函数中。
ts = ts_demuxer_create(onTsPacket, this);
onTsPacket就是回到函数(上下文);
void onTsPacket(void* param, int stream, int codec_id, int flags, int64_t pts, int64_t dts, const void* data, size_t bytes)
{
TsDemuxer *p = (TsDemuxer*) param;
//todo 获取了裸流数据信息,可以变魔术了
}
onTsPacket函数获取的就是裸流信息,可以拿去做处理了,注意这个函数获得数据做后续操作时我们通常需要帧率,帧的类型,针对类型不能用flags判断,这里协议里实际是指帧对齐标志,自己写一个判断函数好了就几行。这个函数每次输出的是一帧的数据,也就是说这个函数调用频率就是裸流帧率,本人的HLS输入流帧率是25,输出的H264是20,使用时可以在onTsPacket函数放个累加测测看。(坑:后续操作如果遇见问题,可以在这个函数中把单帧数据写文件分析一下,一般都是帧类型判断错误,或者帧率不对导致)