效果图如下:
首先就是介绍一下项目中用到的工具 推流工具
和FLV文件分析工具
;
这里必须吐槽一下好用的流媒体分析工具都是windos端上的,MAC OS 上能用的工具少之又少,希望自己有一天也有实力造出来一些好用的工具提供给大家;
预览推流的工具 Railgun Cast;
FLV流解析工具 QHFlvParserMan
进制在线转换工具 https://tool.oschina.net/hexconvert;
转码推流 FFmpeg
视频截图中的人物有点马赛克的的感觉,后来文章写完发现是错误的 ffmpeg命令
生成错误编码的flv导致的;
本文的分析视频文件 编码格式是 视频 h.263
+ 音频 mp3
的格式撰写的;
1.获取flv 文件
ffmpeg -i /Users/pengchao/Documents/unknown.mp4 /Users/pengchao/Documents/download.flv
保证编码格式不变使用该命令
ffmpeg -i /Users/pengchao/Documents/unknown.mp4 -c copy /Users/pengchao/Documents/download.flv
2.开启直播服务器
打开软件 Railgun Cast
获取推流地址 rtmp://192.168.0.100:57139/railgun/test
;
3.解析FLV
在讲代码之前还是按照惯例对流协议进行一下讲解;
在文章 FLV格式解析 中,我们了解到一个FLV
文件是有 FLV Header
和FLV Body
组成的,FLV Body
又由 Video Tag
,Audio Tag
,Script Tag
三种Tag 组成, 更详细的解析可以看这篇文章,加深对FLV 格式的了解FLV格式解析;
重点、重点、重点
Video Tag
,Audio Tag
,Script Tag
都是由Tag Header
组成,三者的Tag Body 结构
不一样;
重点、重点、重点
3.1 FLV Header
FLV Header
占9个字节; 分别代表的含义如下;
通过Xcode 查看内存中文件中的16进制,前9个bytes 是46 4C 56 01 05 00 00 00 09
,通过换算后,正是 FLV Header
;
通过 QHFlvParserMan
工具,也可以看到FLV Header
的9 byte
的内容 并佐证其中代表的含义;
3.2 FLV TAG
参考FLV 格式的定义,FLV Header
后面 应该是一个 FLV TAG
,第一个字节0x12
,通过换算后,这是一个 Scripty Tag
类型的TAG
;
Script Tag
又通常被称为MetadataTag
,会放一些关于FLV视频和音频的元数据信息如:duration、width、height等。通常该类型Tag会跟在FileHeader后面作为第一个Tag出现,而且只有一个。
在本demo 中 script Tag
可以不推到服务器不影响解码,因此 Script Tag
我们跳过;
3.1 FLV TAG【 Script Tag 】
在拿到了 Script Tag
的长度后,移动指针,指向下一个 TAG
, 获取第一个字节为0x12
, 用 10进制表示18
,说明这是一个Script Tag
,(由于 三种 FLV Tag 的格式不一样,所以针对不同的FLV TAG
需要单独解析),参考 Script Tag Body 的格式。11 bytes
的 FLV TAG header
格式一致,后3bytes
为 00 01 A4
表示Script Tag
的长度 ,用10进制表示 420
,因此Script tag
的length是 420, 就知道了指针移动多少距离可以找到下一个TAG
;
参考 QHFlvParserMan
工具,佐证我们格式分析的是否正确;
3.2 FLV TAG【Audio Tag】
指针接着移动 420 + 4
的长度,420 是上一个Script TAG
的长度 ,第二个TAG 的第一个bytes 是 0x08
,表示这是一个Audio Tag
,由于 TAG Header
都是固定的11 位,所以分析起来和 Script Tag
一样,后面紧跟着是3bytes DataSize
、3bytes timeStamp
、1bytes extend TimeStamp
、3bytes streamID
;
重点、重点、重点 Audio Tag Body
格式参考下图
找到 FLV TAG Header
的11 个字节后 08 00 01 A2 00 00 00 00 00 00 00
,紧跟着 第一个字节是 0x2F
用二进制表示 00101111
;
第1、4位bit 0010
表示 内部音频编码格式是mp3
;
第5、6位bit 11
编码采样率 44k-HZ;
第7位1
音频采样深度 16Bit
第8位1
声道数 2;
通过ffprobe
查看 flv 的信息 和上面的信息是 一致的 (Audio 类型,mp3,双声道,44100hz);
Duration: 00:00:30.10, start: 0.000000, bitrate: 2007 kb/s
Stream #0:0: Audio: mp3, 44100 Hz, stereo, fltp, 128 kb/s
Stream #0:1: Video: flv1, yuv420p, 720x1280, 200 kb/s, 30 fps, 30 tbr, 1k tb
音频 Audio Tag Data参考如下图的格式
从 QHFlvParserMan
分析工具获得的数据,可以作证我们格式分析的正确性;
3.2 FLV TAG【Video Tag】
在拿到了上一个 Video Tag
的长度后,移动指针 418个长度,指向下一个TAG, 获取第一个字节,0x09
用10进制表示09
,这是Video Tag
Video Tag Header
也是11 byte 字节表示的,09 00 EC 14 00 00 19 00 00 00 00
;
找到找到Header 后的第一个字节 是 0x12
二进制表示为 00010010
; 参考下图 VideoTag Data 部分的解释
第1到4bit 0001
表示 这是一个关键帧;
第5到6bit 0010
表示这个是 h.263 ;
震惊、震惊、震惊
,竟然碰到了h.263
,这是真的吗 、;通过 ffprobe -show_streams download.flv
命令,我再次确认了一下 flv 文件的Video Stream
的编码格式,发现这竟然是真的(发现原因是我生成 flv 的 ffmpeg 命令没有copy原来的编码方式)
;
index=1
codec_name=flv1
codec_long_name=FLV / Sorenson Spark / Sorenson H.263 (Flash Video)
这里我调整了一下 生成flv 的命令,保证生成的FLV
文件的编码格式和 原始MP4
文件的编码格式不变,音频为AAC
,视频为 H.264
;
ffmpeg -i /Users/pengchao/Documents/unknown.mp4 -c copy /Users/pengchao/Documents/download.flv
编码方式和 MP4 的文件 一致了
Stream #0:0: Video: h264 (High), yuv420p(tv, smpte170m/smpte432/bt709, progressive), 720x1280 [SAR 1:1 DAR 9:16], 1531 kb/s, 28.24 fps, 30 tbr, 1k tbn
Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp, 128 kb/s
Video Tag Data 部分的解释 如下:
4.发送音频包和视频包
本来只是想写一下libRTMP 怎么用的,结果写着写着,变成了格式分析了,那先这样吧;
RTMP_Alloc():
为结构体“RTMP”分配内存。
RTMP_Init():
初始化结构体“RTMP”中的成员变量。
RTMP_SetupURL():
设置输入的RTMP连接的URL。
RTMP_EnableWrite():
发布流的时候必须要使用。如果不使用则代表接收流。
RTMP_Connect():
建立RTMP连接,创建一个RTMP协议规范中的NetConnection。
RTMP_ConnectStream():
创建一个RTMP协议规范中的NetStream。
Delay:
发布流过程中的延时,保证按正常播放速度发送数据。
RTMP_SendPacket():
发送一个RTMP数据RTMPPacket。
RTMP_Close():
关闭RTMP连接。
RTMP_Free():
释放结构体“RTMP”。
具体解析FLV 文件的流程可以参考如下demo源码;整体流程就是,初始化RTMP
上下文,配置必要的参数, 解析FLV
文件,找到FLV Header
,然后找到FLV Tag
,解析FLV Tag
,然后将其封装成 RTMP Packet
,继续找到下一个FLV Tag
解析并封装成RTMP Packet
,如此循环,直到结束;
源码如下:
int push_packet(char* infile,char* rtmp_server_path){
RTMP *rtmp = NULL;
//RTMPPacket *packet = NULL;
uint32_t start_time = 0;
uint32_t now_time = 0;
//上一帧时间戳
long pre_frame_time = 0;
long lasttime = 0;
//下一帧是否是关键帧
int bNextIsKey = 1;
uint32_t preTagSize = 0;
uint32_t type = 0;
uint32_t datalength = 0;
uint32_t timestamp = 0;
uint32_t streamid = 0;
FILE* fp = NULL;
fp = fopen(infile,"rb");
if(!fp) {
RTMP_LogPrintf("Open File Error\n");
return -1;
}
//为结构体“RTMP”分配内存
rtmp = RTMP_Alloc();
if(!rtmp){
RTMP_LogPrintf("RTMP_Alloc failed\n");
return -1;
}
//初始化结构体“RTMP”中的成员变量
RTMP_Init(rtmp);
rtmp->Link.timeout = 5;
if(!RTMP_SetupURL(rtmp, rtmp_server_path)){
RTMP_Log(RTMP_LOGERROR,"SetupURL Err\n");
RTMP_Free(rtmp);
return -1;
}
//发布流的时候必须要使用。如果不使用则代表接收流。
RTMP_EnableWrite(rtmp);
//建立RTMP连接,创建一个RTMP协议规范中的NetConnection
if (!RTMP_Connect(rtmp,NULL)){
RTMP_Log(RTMP_LOGERROR,"Connect Err\n");
RTMP_Free(rtmp);
return -1;
}
//创建一个RTMP协议规范中的NetStream
if (!RTMP_ConnectStream(rtmp,0)){
RTMP_Log(RTMP_LOGERROR,"ConnectStream Err\n");
RTMP_Close(rtmp);
RTMP_Free(rtmp);
return -1;
}
RTMPPacket *packet = malloc(sizeof(RTMPPacket));
RTMPPacket_Alloc(packet,1024*64);
RTMPPacket_Reset(packet);
packet->m_hasAbsTimestamp = 0;
packet->m_nChannel = 0x04;
packet->m_nInfoField2 = rtmp->m_stream_id;
RTMP_LogPrintf("Start to send data ...\n");
//跳过flv的9个头字节
fseek(fp,9,SEEK_SET);
//跳过previousTagSize所占的4个字节
fseek(fp,4,SEEK_CUR);
start_time=RTMP_GetTime();
while(1)
{
//如果下一帧是关键帧,且上一帧的时间戳比现在的时间还长(外部时钟),说明推流速度过快了,可以延时下
if((((now_time=RTMP_GetTime())-start_time)<(pre_frame_time)) && bNextIsKey)
{
//wait for 1 sec if the send process is too fast
//这个机制不好,需要改进
if(pre_frame_time>lasttime){
RTMP_LogPrintf("TimeStamp:%8lu ms\n",pre_frame_time);
lasttime=pre_frame_time;
}
sleep(0.1);//等待1秒
continue;
}
//not quite the same as FLV spec
if(!ReadU8(&type,fp))//读取type
break;
if(!ReadU24(&datalength,fp))//读取datalength的长度
break;
if(!ReadTime(×tamp,fp))//从flv的head读取时间戳
break;
if(!ReadU24(&streamid,fp))//读取streamid的类型
break;
if(type!=0x08&&type!=0x09){//如果不是音频或视频类型
//jump over non_audio and non_video frame,
//jump over next previousTagSizen at the same time
printf("seek offset :%d",(datalength +4));
fseek(fp,datalength+4,SEEK_CUR);//跳过脚本以及脚本后的previousTagSize
continue;
}
//把flv的音频和视频数据写入到packet的body中
if(fread(packet->m_body,1,datalength,fp)!=datalength)
break;
//packet header 大尺寸
packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
packet->m_nTimeStamp = timestamp;//时间戳
packet->m_packetType = type;//包类型
packet->m_nBodySize = datalength;//包大小
pre_frame_time=timestamp;//把包读取到的时间戳赋值给上一帧时间戳变量
if (!RTMP_IsConnected(rtmp)){//确认连接
RTMP_Log(RTMP_LOGERROR,"rtmp is not connect\n");
break;
}
RTMP_Log(RTMP_LOGINFO,"send packet\n");
//发送一个RTMP数据RTMPPacket
if (!RTMP_SendPacket(rtmp,packet,0)) {
RTMP_Log(RTMP_LOGERROR,"Send Error\n");
break;
}
if(!ReadU32(&preTagSize,fp))
break;
if(!PeekU8(&type,fp))//去读下一帧的类型
break;
if(type==0x09){//如果下一帧是视频
if(fseek(fp,11,SEEK_CUR)!=0)
break;
if(!PeekU8(&type,fp)){
break;
}
if(type==0x17)//如果视频帧是关键帧
bNextIsKey=1;
else
bNextIsKey=0;
fseek(fp,-11,SEEK_CUR);
}
}
RTMP_LogPrintf("\nSend Data Over!\n");
if(fp) {
fclose(fp);
}
if (rtmp!=NULL){
//关闭RTMP连接
RTMP_Close(rtmp);
//释放结构体RTMP
RTMP_Free(rtmp);
rtmp=NULL;
}
if (packet!=NULL){
RTMPPacket_Free(packet);
free(packet);
packet=NULL;
}
return 0;
}
5.总结
源码地址 https://github.com/hunter858/OpenGL_Study/AVFoundation/libRTMP_demo
本文创作参考文章: 基于librtmp的推流实现