Flv与Rtmp
Flv和Rtmp封装格式转化
一.Flv介绍
FLV(Flash Video)是Adobe公司推出的⼀种流媒体格式,由于其封装后的⾳视频⽂件体积⼩、 封装简单等特点,⾮常适合于互联⽹上使⽤。⽬前主流的视频⽹站基本都⽀持FLV。采⽤FLV 格式封装的⽂件后缀为.flv。
二.Flv格式
1.FLV封装格式是由⼀个⽂件头(file header)和 ⽂件体(file Body)组成。
2.FLV body由⼀ 对对的(Previous Tag Size字段 + tag)组成。Previous Tag Size字段 排列在Tag之前,占⽤4 个字节。Previous Tag Size记录了前⾯⼀个Tag的⼤⼩,⽤于逆向读取处理。(Seek比较方便)
3.FLV header后 的第⼀个Pervious Tag Size的值为0。
其中,Previous Tag Size 占用4个字节,记录前一个 Tag 的大小,用于逆向读取处理。
FLV header 后的第一个 Previous Tag Size 为 0。
Tag 由 Tag Header 和 Tag Body 组成。Tag Header 占 11 字节。
而 Tag Body 一般分为三种类型:脚本(Script)数据、视频数据、音频数据。其中 Video Tag 和 Audio Tag 由 Tag header 和 Tag Data 组成;Script Tag 只有 Tag Data。
FLV 数据以大端序进行存储,在解析时需要注意。
FLV 文件的详细内容结构如下:
- FLV header
从上图中可以看到,FLV 头占用 9 个字节,用来标识文件类型为 FLV 类型,以及后续的音视频标识。一个 FLV 文件,每种类型的 tag 都属于一个流,也就是一个 flv 文件最多只有一个音频流,一个视频流,不存在多个独立的音视频流在一个文件的情况。FLV 头的结构如下:
参考ZLMedia中FlvHeader定义,常将FlvHeader与4字节长度合并到一起定义结构如下:注意区分大小端
windows需要加上#pragma pack(push, 1) 参考如下代码
#pragma pack(push, 1)
class FLVHeader {
public:static constexpr uint8_t kFlvVersion = 1;static constexpr uint8_t kFlvHeaderLength = 9;//FLVchar flv[3];//File version (for example, 0x01 for FLV version 1)uint8_t version;
#if __BYTE_ORDER == __BIG_ENDIAN// 保留,置0 [AUTO-TRANSLATED:46985374]// Preserve, set to 0uint8_t : 5;// 是否有音频 [AUTO-TRANSLATED:9467870a]// Whether there is audiouint8_t have_audio : 1;// 保留,置0 [AUTO-TRANSLATED:46985374]// Preserve, set to 0uint8_t : 1;// 是否有视频 [AUTO-TRANSLATED:42d0ed81]// Whether there is videouint8_t have_video : 1;
#else// 是否有视频 [AUTO-TRANSLATED:42d0ed81]// Whether there is videouint8_t have_video : 1;// 保留,置0 [AUTO-TRANSLATED:46985374]// Preserve, set to 0uint8_t : 1;// 是否有音频 [AUTO-TRANSLATED:9467870a]// Whether there is audiouint8_t have_audio : 1;// 保留,置0 [AUTO-TRANSLATED:46985374]// Preserve, set to 0uint8_t : 5;
#endif// The length of this header in bytes,固定为9 [AUTO-TRANSLATED:126988fc]// The length of this header in bytes, fixed to 9uint32_t length;// 固定为0 [AUTO-TRANSLATED:d266c0a7]// Fixed to 0uint32_t previous_tag_size0;
};
#pragma pack(pop)
FLV body
FLV Header之后,就是 FLV File Body。FLV File Body 是由一连串的 back-pointers + tags 构成。
back-pointers 指的是上一个Tag的大小,Previous Tag Size永远是0, TagSize 的作用方便文件从后面向前查找例如Seek
FLV tag
tag 表示音视频数据,每一个 tag 由两部分组成:tag header 和 tag data。
Flv Tag Header定义,一般Flv tag与rtmp tag保持一致,参考ZLMediaKit定义结构体如下:
class RtmpTagHeader {
public:uint8_t type = 0;uint8_t data_size[3] = {0};uint8_t timestamp[3] = {0};uint8_t timestamp_ex = 0;uint8_t streamid[3] = {0}; /* Always 0. */
};
上面 Timestamp 和 TimestampExtended 两个字段拼成一个 32 位的时间戳,是当前 Tag 的解码时间戳 (DTS)。对于音频帧来说,PTS 和 DTS 相同。
对于视频帧来说,若含 B 帧,则 PTS 和 DTS 不同,H264 视频帧 PTS = DTS + CTS,CTS 就是 CompositionTime 字段,表示 PTS 与 DTS 的时间偏移值,单位 ms。
tag header 一般占 11 个字节。
Flv Tag Header读取实例,因为是Flv Tag Header读取,结构体可以不用严格按照11字节创建结构体。
#define FLV_HEADER_SIZE 9 // DataOffset included
#define FLV_TAG_HEADER_SIZE 11 // StreamID included// FLV Tag Type
#define FLV_TYPE_AUDIO 8
#define FLV_TYPE_VIDEO 9
#define FLV_TYPE_SCRIPT 18struct FlvTagHeader
{uint8_t filter; // 0-No pre-processing requireduint8_t type; // 8-audio, 9-video, 18-script datauint32_t size; // data sizeuint32_t timestamp;uint32_t streamId;
};int RecordReaderFlv::FlvTagHeaderRead(struct FlvTagHeader* tag, const uint8_t* buf, size_t len)
{if (len < FLV_TAG_HEADER_SIZE){assert(0);return -1;}// TagTypetag->type = buf[0] & 0x1F;tag->filter = (buf[0] >> 5) & 0x01;assert(FLV_TYPE_VIDEO == tag->type || FLV_TYPE_AUDIO == tag->type || FLV_TYPE_SCRIPT == tag->type);// DataSizetag->size = ((uint32_t)buf[1] << 16) | ((uint32_t)buf[2] << 8) | buf[3];// TimestampExtended | Timestamptag->timestamp = ((uint32_t)buf[4] << 16) | ((uint32_t)buf[5] << 8) | buf[6] | ((uint32_t)buf[7] << 24);// StreamID Always 0tag->streamId = ((uint32_t)buf[8] << 16) | ((uint32_t)buf[9] << 8) | buf[10];//assert(0 == tag->streamId);return FLV_TAG_HEADER_SIZE;
}
Flv Tag Header 写入实例:
void set_be24(void *p, uint32_t val)
{uint8_t *data = (uint8_t *) p;data[0] = val >> 16;data[1] = val >> 8;data[2] = val;
}void FlvMuxer::onWriteFlvTag(uint8_t type, const Buffer::Ptr &buffer, uint32_t time_stamp, bool flush) {RtmpTagHeader header;header.type = type;set_be24(header.data_size, (uint32_t) buffer->size());header.timestamp_ex = (time_stamp >> 24) & 0xff;set_be24(header.timestamp, time_stamp & 0xFFFFFF);//tag headeronWrite(obtainBuffer((char *) &header, sizeof(header)), false);//tag dataonWrite(buffer, false);//PreviousTagSizeuint32_t size = htonl((uint32_t) (buffer->size() + sizeof(header)));onWrite(obtainBuffer((char *) &size, 4), flush);
}
FLV tag 的类型可以是视频、音频和 Script(脚本类型)。
FLV tag data:audio tag
audio tag 包含 audio tag header 和 audio tag body 两部分。
其中,audio tag header 占 1 字节,包含了音频数据的参数信息,从第 2 个字节开始为音频流数据。
前 4bit 表示音频格式;第 5、6bit 表示采样率;第 7bit 表示采样精度;第 8bit 表示音频声道。
这里着重强调一下格式10格式:AAC。声音类型应为 1 (立体声) 且采样率应为 3 (44 kHz)。这并不表示 FLV 中的 AAC 音频总是立体声、44 kHz的数据。实际上,Flash 播放器会忽略这两个值,而从已编码的 AAC 位流中提取出声道数和采样率信息。
第二个字节开始为音频数据:
如果音频格式为 10,即是 AAC 格式。AudioTagHeader 中会多出一个字节 AACPacketType,这个字段来表示 AACAUDIODATA 的类型:0 = AAC sequence header,1 = AAC raw。
AAC sequence header 也就是包含了 AudioSpecificConfig,里面有一些更加详细音频的信息。
AAC raw 这种包含的就是音频 ES 流了,也就是音频负载(audio payload)。
FLV tag data:video tag
video tag 包含 video tag header 和 video tag body 两部分。video tag data 开始的第一个字节包含视频数据的参数信息,从第二个字节开始为视频流数据。结构如下:
FLV 的写流程
参考ZLMediaKit FlvMuxer,写Flv主要流程
void FlvMuxer::onWriteFlvHeader(const RtmpMediaSource::Ptr &src) {// 发送flv文件头 [AUTO-TRANSLATED:ee2c5556]// Send the flv file header.auto buffer = obtainBuffer();buffer->setCapacity(sizeof(FLVHeader));buffer->setSize(sizeof(FLVHeader));FLVHeader *header = (FLVHeader *) buffer->data();memset(header, 0, sizeof(FLVHeader));header->flv[0] = 'F';header->flv[1] = 'L';header->flv[2] = 'V';header->version = FLVHeader::kFlvVersion;header->length = htonl(FLVHeader::kFlvHeaderLength);header->have_video = src->haveVideo();header->have_audio = src->haveAudio();// memset时已经赋值为0 [AUTO-TRANSLATED:0f71eef1]// It has already been assigned to 0 during memset.//header->previous_tag_size0 = 0;//flv headeronWrite(buffer, false);// metadatasrc->getMetaData([&](const AMFValue &metadata) {AMFEncoder invoke;invoke << "onMetaData" << metadata;onWriteFlvTag(MSG_DATA, std::make_shared<BufferString>(invoke.data()), 0, false);});//config framesrc->getConfigFrame([&](const RtmpPacket::Ptr &pkt) {onWriteRtmp(pkt, true);});
}void FlvMuxer::onWriteFlvTag(const RtmpPacket::Ptr &pkt, uint32_t time_stamp, bool flush) {onWriteFlvTag(pkt->type_id, pkt, time_stamp, flush);
}void FlvMuxer::onWriteFlvTag(uint8_t type, const Buffer::Ptr &buffer, uint32_t time_stamp, bool flush) {RtmpTagHeader header;header.type = type;set_be24(header.data_size, (uint32_t) buffer->size());header.timestamp_ex = (time_stamp >> 24) & 0xff;set_be24(header.timestamp, time_stamp & 0xFFFFFF);//tag headeronWrite(obtainBuffer((char *) &header, sizeof(header)), false);//tag dataonWrite(buffer, false);//PreviousTagSizeuint32_t size = htonl((uint32_t) (buffer->size() + sizeof(header)));onWrite(obtainBuffer((char *) &size, 4), flush);
}void FlvMuxer::onWriteRtmp(const RtmpPacket::Ptr &pkt, bool flush) {onWriteFlvTag(pkt, pkt->time_stamp, flush);
}
参考链接:https://blog.csdn.net/ProgramNovice/article/details/137468819
三.Rtmp协议格式
Message介绍
1、消息(Message)是RTMP协议中基本的数据单元。由Message Header和Message Payload(可以理解成message body)组成。
2、对于音视频数据而言每一个message就是一帧数据。对于flv的tag而言,就是对应rtmp每个message,一个tag就是一个message,是一一对应的关系;相当于每一个tag都封装成一个message。message payload的数据格式和tag data的数据格式是相同的,message header和tag header的格式不同。
3、Message Format如下:
Field | Type | Comment | |
---|---|---|---|
Message Header | Length | 3 bytes | Message Payload(消息负载)的长度,不包含Message Header |
Timestamp | 4 bytes | 时间戳(既是pts也是dts,因为直播场景中没有B帧,所以pts=dts) | |
Message [Type Id](https://so.csdn.net/so/search?q=Type Id&spm=1001.2101.3001.7020) | 1 bytes | 消息类型,主要包括协议控制消息、音视频消息、命令消息等 | |
Message Stream Id | 3 bytes | 消息流ID可以是任意值。不同的message可以有相同的值。复用到同一块流上的不同消息流基于它们的消息流ID解复用。 | |
Message Payload | — | n bytes | 是消息中包含的实际数据,消息类型不同payload大小也不同。例如,它可以是一些音频样本或压缩视频数据或Metadata等 |
4、多路复用,RTMP可以将来自不同视频流的切片(chunk)在单个连接上传输,这种方法被称为“多路复用”,不同的流就用不同的Message Stream Id区分。
详细参考链接: https://blog.csdn.net/weixin_39399492/article/details/128069969
Chunk介绍
1、RTMP以Message为基本单位,通过把Message拆分成Chunk来进行网络发送。chunk data默认是128字节。chunk是RTMP最小的传输单元。目的是:防止一个大的数据包传输时间过长,阻塞其它数据包的传输。chunk合成message:接收端将接收到chunk的chunk data的大小加和,如果等于message payload(通过chunk->message header->message length获取)的则认为是同一个message。
2、Chunk在传输时:同一个Message产生的多个Chunk只会串行发送。先发送的Chunk一定先到达。不同Message产生的Chunk可以并行发送。并行发送的Chunk复用了一条TCP链接。
3、Chunk Format如下:
Field | Type | Comment | |
---|---|---|---|
Chunk Header | Basic Header | 1-3 bytes | 包含fmt(chunk type)和chunk stream id(csid),其中fmt决定了chunk的类型及message header的长度,占2 bit,而Basic header的长度取决于csid的数值大小,最少占1 byte。 |
Message Header | 0,3,7 or 11 bytes | 要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。 长度取决于Basic Header中的chunk type,有Type 0,1,2, | |
Extended Timestamp | 0 or 4 bytes | 扩展时间戳(0 bytes时表示此字段不存在) | |
Chunk Data | — | n bytes | 是消息中包含的实际数据,消息类型不同data大小也不同 |
4、有多种chunk type的目的是:减少重复数据发送,提高chunk data的占比。
Basic Header
1.Basic Header
Field | Type | Comment | |
---|---|---|---|
fmt | 2 bits | 表示chunk type,取值[0, 3],即chunk共有4种类型 | |
csid | 6,14 or 22 bits | csid范围是365599,02为协议保留用作特殊信息;**通常控制流csid为2,命令流为3,开发中发现音视频流csid可自定义,如音频流4,视频流6.**上文提到Basic Header大小为1-3 bytes,由于fmt域占2bits,所以CSID长度分别是6 bits、14 bits或22 bits |
2. Message Header
1、Message Header的格式和长度取决于Basic Header的chunk type,即fmt,fmt取值[0-3],所以共有4种不同的chunk格式,目的是减少重复数据发送,提高 chunk data的占比。同时也有4种不同的Message Header。
2、Message Header结构如下:
(1) chunk type = 0(fmt) = 0:11 bytes
.0 1 2 30 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| timestamp |message length |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| message length (coutinue) |message type id| msg stream id |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| msg stream id |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
type=0时Message Header占用11个字节,其他三种能表示的数据它都能表示,但在chunk stream 的开始第一个chunk和头信息
中的时间戳后退(即值与上一个chunk相比减小,通常在回退播放的时候会出现这种情况)的时候必须采用这种格式。
- timestamp(时间戳):占用3个字节,因此它最多能表示到16777215=0xFFFFFF=2^24-1,当它
的值超过这个最大值时,这三个字节都置为1,这样实际的timestamp会转存到 Extended
Timestamp 字段中,接收端在判断timestamp字段24个位都为1时就会去Extended Timestamp
中解析实际的时间戳。 - message length(消息数据长度):占用3个字节,表示实际发送的消息的数据如音频帧、视频
帧等数据的长度,单位是字节。注意这里是Message的长度,也就是chunk属于的Message的总长
度,而不是chunk本身data的长度。 - message type id(消息的类型id):1个字节,表示实际发送的数据的类型,如8代表音频数据,
9代表视频数据。 - message stream id(消息的流id):4个字节,表示该chunk所在的流的ID,和Basic Header
的CSID一样,它采用小端存储方式。
(2) Chunk Type(fmt) = 1:7 bytes
.0 1 2 30 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| timestamp delta |message length |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| message length (coutinue) |message type id|+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
type为1时占用7个字节,省去了表示message stream id的4个字节,表示此chunk和上一次发的 chunk 所在的流相同,如果在
发送端和对端有一个流链接的时候可以尽量采取这种格式。
-
timestamp delta:3 bytes,这里和type=0时不同,存储的是和上一个chunk的时间差。类似
上面提到的timestamp,当它的值超过3个字节所能表示的最大值时,三个字节都置为1,实际
的时间戳差值就会转存到Extended Timestamp字段中,接收端在判断timestamp delta字段24
个bit都为1时就会去Extended Timestamp 中解析实际的与上次时间戳的差值。 -
其他字段与上面的解释相同.
(3) Chunk Type(fmt) = 2:3 bytes
.0 1 2 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+| timestamp delta |+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
type 为 2 时占用 3 个字节,相对于 type = 1 格式又省去了表示消息长度的3个字节和表示消息类型的1个字节,表示此 chunk
和上一次发送的 chunk 所在的流、消息的长度和消息的类型都相同。余下的这三个字节表示 timestamp delta,使用同type=1。
(4)Chunk Type(fmt) = 3: 0 byte
type=3时,为0字节,表示这个chunk的Message Header和上一个是完全相同的。当它跟在type=0的chunk后面时,表示和前一
个 chunk 的时间戳都是相同。什么时候连时间戳都是相同呢?就是一个 Message 拆分成多个 chunk,这个 chunk 和上
一个 chunk 同属于一个 Message。而当它跟在 type = 1或 type = 2 的chunk后面时的chunk后面时,表示和前一个 chunk
的时间戳的差是相同的。比如第一个 chunk 的 type = 0,timestamp = 100,第二个 chunk 的 type = 2,
timestamp delta = 20,表示时间戳为 100 + 20 = 120,第三个 chunk 的 type = 3,表示 timestamp delta = 20,
时间戳为 120 + 20 = 140。
参考链接:https://www.cnblogs.com/jimodetiantang/p/8974075.html
FLV转Rtmp Decoder解析
Flv与Rtmp内容一样只是,header不一致。
Rtmp定义如下:
#pragma pack(1)struct RtmpMessageHeader
{uint8_t timestamp[3];uint8_t length[3];uint8_t type_id;uint8_t stream_id[4];
};// chunk header: basic_header + rtmp_message_header
class RtmpMessage
{
public:using Ptr = shared_ptr<RtmpMessage>;void clear(){index = 0;timestamp = 0;extend_timestamp = 0;if (length > 0 || payload) {length = 0;payload = nullptr;}}bool isCompleted() const{if (length > 0 && index == length && payload) {return true;}return false;}bool isKeyFrame() const{bool isEnhance = (payload->data()[0] >> 4) & 0b1000;uint8_t frame_type;uint8_t packet_type;if (isEnhance) {frame_type = (payload->data()[0] >> 4) & 0b0111;packet_type = payload->data()[0] & 0x0f;return type_id == RTMP_VIDEO && frame_type == 1 && (packet_type == 1 || packet_type == 3);} else {frame_type = (payload->data()[0] >> 4) & 0x0f;packet_type = payload->data()[1];return type_id == RTMP_VIDEO && frame_type == 1 && packet_type == 1;}}public:int trackIndex_;// chunk stream ID(流通道id)和chunk type(即fmt),chunk stream id 一般被简写为CSIDuint8_t type_id = 0; uint8_t codecId = 0;uint8_t csid = 0;uint32_t index = 0;uint32_t timestamp = 0;uint32_t length = 0;uint32_t stream_id = 0;uint32_t extend_timestamp = 0;uint64_t abs_timestamp = 0;uint64_t laststep = 0;StreamBuffer::Ptr payload = nullptr;
};#pragma pack()
实现转换的实例:
void RecordReaderFlv::handleVideo(const char* data, int len)
{if (!_validVideoTrack) {return ;}weak_ptr<RecordReaderFlv> wSelf = dynamic_pointer_cast<RecordReaderFlv>(shared_from_this());uint8_t type = RTMP_VIDEO;uint8_t *payload = (u_char*)data + 11;uint32_t length = len - 11;uint8_t frame_type = (payload[0] >> 4) & 0x0f;uint8_t codec_id = payload[0] & 0x0f;uint32_t timestamp = readUint24BE(data + 4); //扩展字段也读了logTrace << "timestamp: " << timestamp;timestamp |= ((data[8]) << 24);if (!_rtmpVideoDecodeTrack) {_rtmpVideoDecodeTrack = make_shared<RtmpDecodeTrack>(VideoTrackType);if (_rtmpVideoDecodeTrack->createTrackInfo(VideoTrackType, codec_id) != 0) {_validVideoTrack = false;return;}_rtmpVideoDecodeTrack->setOnFrame([wSelf](const FrameBuffer::Ptr& frame) {auto self = wSelf.lock();if (!self) {return;}lock_guard<mutex> lck(self->_mtxFrameList);logTrace << "###input frame flv";self->_frameList.push_back(frame);});if (_onTrackInfo) {_onTrackInfo(_rtmpVideoDecodeTrack->getTrackInfo());}_rtmpVideoDecodeTrack->startDecode();}// flv解析到Rtmp 转为Rtmp Messageauto msg = make_shared<RtmpMessage>();msg->payload = make_shared<StreamBuffer>(length + 1);memcpy(msg->payload->data(), payload, length);msg->abs_timestamp = timestamp;msg->length = length;msg->type_id = RTMP_VIDEO;msg->csid = RTMP_CHUNK_VIDEO_ID;if (!_avcHeader && frame_type == 1/* && codec_id == RTMP_CODEC_ID_H264*/) {// logInfo << "payload[1] : " << (int)payload[1];if (payload[1] == 0) {// sps pps??_avcHeaderSize = length;_avcHeader = make_shared<StreamBuffer>(length + 1);memcpy(_avcHeader->data(), msg->payload->data(), length);type = RTMP_AVC_SEQUENCE_HEADER;_rtmpVideoDecodeTrack->setConfigFrame(msg);}}msg->trackIndex_ = VideoTrackType;_rtmpVideoDecodeTrack->onRtmpPacket(msg);logTrace << "_rtmpVideoDecodeTrack decodeRtmp";_rtmpVideoDecodeTrack->decodeRtmp(msg);}
完整开源代码实现链接:https://gitee.com/inyeme/simple-media-server