总体流程
了解了FLV的封装原理,我们通过⼀个简单的FLV解析器的例⼦来看看FLV到底是怎么样封装/ 解封装的 该FLV的地址:功能强大的 FLV 文件分析和解析器
这个程序能够做什么:
(1)不能直接商⽤,因为每个tag解析后都存放到内存;
(2)audio parse
(3)video parse
(4)script parse
main函数
流程:
1、读取输⼊⽂件(flv类型的视频⽂件)
2、调⽤Process进⾏处理
3、退出
int main(int argc, char *argv[])
{
cout << "Hi, this is FLV parser test program!\n";
if (argc != 3)
{
cout << "FlvParser.exe [input flv] [output flv]" << endl;
return 0;
}
fstream fin;
fin.open(argv[1], ios_base::in | ios_base::binary);
if (!fin)
return 0;
Process(fin, argv[2]);
fin.close();
return 1;
}
处理函数Process
流程:
- 读取⽂件
- 开始解析
- 打印解析信息
- 把解析之后的数据输出到另外⼀个⽂件中
void Process(fstream &fin, const char *filename)
{
CFlvParser parser;
int nBufSize = 2*1024 * 1024;
int nFlvPos = 0;
uint8_t *pBuf, *pBak;
pBuf = new uint8_t[nBufSize];
pBak = new uint8_t[nBufSize];
while (1)
{
int nReadNum = 0;
int nUsedLen = 0;
fin.read((char *)pBuf + nFlvPos, nBufSize - nFlvPos);
nReadNum = fin.gcount();
if (nReadNum == 0)
break;
nFlvPos += nReadNum;
parser.Parse(pBuf, nFlvPos, nUsedLen);
if (nFlvPos != nUsedLen)
{
memcpy(pBak, pBuf + nUsedLen, nFlvPos - nUsedLen);
memcpy(pBuf, pBak, nFlvPos - nUsedLen);
}
nFlvPos -= nUsedLen;
}
parser.PrintInfo();
parser.DumpH264("parser.264");
parser.DumpAAC("parser.aac");
//dump into flv
parser.DumpFlv(filename);
delete []pBak;
delete []pBuf;
}
解析函数
流程:
- 解析flv的头部
- 解析flv的Tag
int CFlvParser::Parse(uint8_t *pBuf, int nBufSize, int &nUsedLen)
{
int nOffset = 0;
if (_pFlvHeader == 0)
{
CheckBuffer(9);
_pFlvHeader = CreateFlvHeader(pBuf+nOffset);
nOffset += _pFlvHeader->nHeadSize;
}
while (1)
{
CheckBuffer(15); // nPrevSize(4字节) + Tag header(11字节)
int nPrevSize = ShowU32(pBuf + nOffset);
nOffset += 4;
Tag *pTag = CreateTag(pBuf + nOffset, nBufSize-nOffset);
if (pTag == NULL)
{
nOffset -= 4;
break;
}
nOffset += (11 + pTag->_header.nDataSize);
_vpTag.push_back(pTag);
}
nUsedLen = nOffset;
return 0;
}
FLV相关的数据结构
CFlvParser表示FLV解析器
FLV由FLV头部和FLV体构成,其中FLV体是由⼀系列的FLV tag构成的
class CFlvParser
{
public:
CFlvParser();
virtual ~CFlvParser();
int Parse(uint8_t *pBuf, int nBufSize, int &nUsedLen);
int PrintInfo();
int DumpH264(const std::string &path);
int DumpAAC(const std::string &path);
int DumpFlv(const std::string &path);
private:
// FLV头
typedef struct FlvHeader_s
{
int nVersion; // 版本
int bHaveVideo; // 是否包含视频
int bHaveAudio; // 是否包含音频
int nHeadSize; // FLV头部长度
/*
** 指向存放FLV头部的buffer
** 上面的三个成员指明了FLV头部的信息,是从FLV的头部中“翻译”得到的,
** 真实的FLV头部是一个二进制比特串,放在一个buffer中,由pFlvHeader成员指明
*/
uint8_t *pFlvHeader;
} FlvHeader;
// Tag头部
struct TagHeader
{
int nType; // 类型
int nDataSize; // 标签body的大小
int nTimeStamp; // 时间戳
int nTSEx; // 时间戳的扩展字节
int nStreamID; // 流的ID,总是0
uint32_t nTotalTS; // 完整的时间戳nTimeStamp和nTSEx拼装
TagHeader() : nType(0), nDataSize(0), nTimeStamp(0), nTSEx(0), nStreamID(0), nTotalTS(0) {}
~TagHeader() {}
};
class Tag
{
public:
Tag() : _pTagHeader(NULL), _pTagData(NULL), _pMedia(NULL), _nMediaLen(0) {}
void Init(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen);
TagHeader _header;
uint8_t *_pTagHeader; // 指向标签头部
uint8_t *_pTagData; // 指向标签body,原始的tag data数据
uint8_t *_pMedia; // 指向标签的元数据,改造后的数据
int _nMediaLen; // 数据长度
};
class CVideoTag : public Tag
{
public:
/**
* @brief CVideoTag
* @param pHeader
* @param pBuf 整个tag的起始地址
* @param nLeftLen
* @param pParser
*/
CVideoTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
int _nFrameType; // 帧类型
int _nCodecID; // 视频编解码类型
int ParseH264Tag(CFlvParser *pParser);
int ParseH264Configuration(CFlvParser *pParser, uint8_t *pTagData);
int ParseNalu(CFlvParser *pParser, uint8_t *pTagData);
};
FlvHeader表示FLV的头部
// FLV头
typedef struct FlvHeader_s
{
int nVersion; // 版本
int bHaveVideo; // 是否包含视频
int bHaveAudio; // 是否包含音频
int nHeadSize; // FLV头部长度
/*
** 指向存放FLV头部的buffer
** 上面的三个成员指明了FLV头部的信息,是从FLV的头部中“翻译”得到的,
** 真实的FLV头部是一个二进制比特串,放在一个buffer中,由pFlvHeader成员指明
*/
uint8_t *pFlvHeader;
} FlvHeader;
标签
标签包括标签头部和标签体,根据类型的不同,标签体可以分成三种:
- script类型的标签
- ⾳频标签
- 视频标签
标签头部
// Tag头部
struct TagHeader
{
int nType; // 类型
int nDataSize; // 标签body的大小
int nTimeStamp; // 时间戳
int nTSEx; // 时间戳的扩展字节
int nStreamID; // 流的ID,总是0
uint32_t nTotalTS; // 完整的时间戳nTimeStamp和nTSEx拼装
TagHeader() : nType(0), nDataSize(0), nTimeStamp(0), nTSEx(0), nStreamID(0), nTotalTS(0) {}
~TagHeader() {}
};
标签数据
class Tag
{
public:
Tag() : _pTagHeader(NULL), _pTagData(NULL), _pMedia(NULL), _nMediaLen(0) {}
void Init(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen);
TagHeader _header;
uint8_t *_pTagHeader; // 指向标签头部
uint8_t *_pTagData; // 指向标签body,原始的tag data数据
uint8_t *_pMedia; // 指向标签的元数据,改造后的数据
int _nMediaLen; // 数据长度
};
script类型的标签
class CMetaDataTag : public Tag
{
public:
CMetaDataTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
double hexStr2double(const unsigned char* hex, const unsigned int length);
int parseMeta(CFlvParser *pParser);
void printMeta();
uint8_t m_amf1_type;
uint32_t m_amf1_size;
uint8_t m_amf2_type;
unsigned char *m_meta;
unsigned int m_length;
double m_duration;
double m_width;
double m_height;
double m_videodatarate;
double m_framerate;
double m_videocodecid;
double m_audiodatarate;
double m_audiosamplerate;
double m_audiosamplesize;
bool m_stereo;
double m_audiocodecid;
string m_major_brand;
string m_minor_version;
string m_compatible_brands;
string m_encoder;
double m_filesize;
};
⾳频标签
class CAudioTag : public Tag
{
public:
CAudioTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
int _nSoundFormat; // 音频编码类型
int _nSoundRate; // 采样率
int _nSoundSize; // 精度
int _nSoundType; // 类型
// aac
static int _aacProfile; // 对应AAC profile
static int _sampleRateIndex; // 采样率索引
static int _channelConfig; // 通道设置
int ParseAACTag(CFlvParser *pParser);
int ParseAudioSpecificConfig(CFlvParser *pParser, uint8_t *pTagData);
int ParseRawAAC(CFlvParser *pParser, uint8_t *pTagData);
};
视频标签
class CVideoTag : public Tag
{
public:
/**
* @brief CVideoTag
* @param pHeader
* @param pBuf 整个tag的起始地址
* @param nLeftLen
* @param pParser
*/
CVideoTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
int _nFrameType; // 帧类型
int _nCodecID; // 视频编解码类型
int ParseH264Tag(CFlvParser *pParser);
int ParseH264Configuration(CFlvParser *pParser, uint8_t *pTagData);
int ParseNalu(CFlvParser *pParser, uint8_t *pTagData);
};
解析FLV头部
入口函数
int CFlvParser::Parse(uint8_t *pBuf, int nBufSize, int &nUsedLen)
{
int nOffset = 0;
if (_pFlvHeader == 0)
{
CheckBuffer(9);
_pFlvHeader = CreateFlvHeader(pBuf+nOffset);
nOffset += _pFlvHeader->nHeadSize;
}
while (1)
{
CheckBuffer(15); // nPrevSize(4字节) + Tag header(11字节)
int nPrevSize = ShowU32(pBuf + nOffset);
nOffset += 4;
Tag *pTag = CreateTag(pBuf + nOffset, nBufSize-nOffset);
if (pTag == NULL)
{
nOffset -= 4;
break;
}
nOffset += (11 + pTag->_header.nDataSize);
_vpTag.push_back(pTag);
}
nUsedLen = nOffset;
return 0;
}
FLV头部解析函数
CFlvParser::FlvHeader *CFlvParser::CreateFlvHeader(uint8_t *pBuf)
{
FlvHeader *pHeader = new FlvHeader;
pHeader->nVersion = pBuf[3]; // 版本号
pHeader->bHaveAudio = (pBuf[4] >> 2) & 0x01; // 是否有音频
pHeader->bHaveVideo = (pBuf[4] >> 0) & 0x01; // 是否有视频
pHeader->nHeadSize = ShowU32(pBuf + 5); // 头部长度
pHeader->pFlvHeader = new uint8_t[pHeader->nHeadSize];
memcpy(pHeader->pFlvHeader, pBuf, pHeader->nHeadSize);
return pHeader;
}
解析标签头部
标签的解析过程
- CFlvParser::Parse调⽤CreateTag解析标签
- CFlvParser::CreateTag⾸先解析标签头部
- 根据标签头部的类型字段,判断标签的类型
- 如果是视频标签,那么解析视频标签
- 如果是⾳频标签,那么解析⾳频标签
- 如果是其他的标签,那么调⽤Tag::Init进⾏解析
解析标签头部的函数
CFlvParser::Tag *CFlvParser::CreateTag(uint8_t *pBuf, int nLeftLen)
{
// 开始解析标签头部
TagHeader header;
header.nType = ShowU8(pBuf+0); // 类型
header.nDataSize = ShowU24(pBuf + 1); // 标签body的长度
header.nTimeStamp = ShowU24(pBuf + 4); // 时间戳 低24bit
header.nTSEx = ShowU8(pBuf + 7); // 时间戳的扩展字段, 高8bit
header.nStreamID = ShowU24(pBuf + 8); // 流的id
header.nTotalTS = (uint32_t)((header.nTSEx << 24)) + header.nTimeStamp;
// 标签头部解析结束
// cout << "total TS : " << header.nTotalTS << endl;
// cout << "nLeftLen : " << nLeftLen << " , nDataSize : " << header.nDataSize << endl;
if ((header.nDataSize + 11) > nLeftLen)
{
return NULL;
}
Tag *pTag;
switch (header.nType) {
case 0x09: // 视频类型的Tag
pTag = new CVideoTag(&header, pBuf, nLeftLen, this);
break;
case 0x08: // 音频类型的Tag
pTag = new CAudioTag(&header, pBuf, nLeftLen, this);
break;
case 0x12: // script Tag
pTag = new CMetaDataTag(&header, pBuf, nLeftLen, this);
break;
default: // script类型的Tag
pTag = new Tag();
pTag->Init(&header, pBuf, nLeftLen);
}
return pTag;
}
视频标签
class CVideoTag : public Tag
{
public:
/**
* @brief CVideoTag
* @param pHeader
* @param pBuf 整个tag的起始地址
* @param nLeftLen
* @param pParser
*/
CVideoTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser);
int _nFrameType; // 帧类型
int _nCodecID; // 视频编解码类型
int ParseH264Tag(CFlvParser *pParser);
int ParseH264Configuration(CFlvParser *pParser, uint8_t *pTagData);
int ParseNalu(CFlvParser *pParser, uint8_t *pTagData);
};
创建视频标签
流程如下:
- 初始化
- 解析帧类型
- 解析视频编码类型
- 解析视频标签
CFlvParser::CVideoTag::CVideoTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser)
{
// 初始化
Init(pHeader, pBuf, nLeftLen);
uint8_t *pd = _pTagData;
_nFrameType = (pd[0] & 0xf0) >> 4; // 帧类型
_nCodecID = pd[0] & 0x0f; // 视频编码类型
// 开始解析
if (_header.nType == 0x09 && _nCodecID == 7)
{
ParseH264Tag(pParser);
}
}
解析视频标签
流程如下:
- 解析数据包类型
- 如果数据包是配置信息,那么就解析配置信息
- 如果数据包是视频数据,那么就解析视频数据
int CFlvParser::CVideoTag::ParseH264Tag(CFlvParser *pParser)
{
uint8_t *pd = _pTagData;
/*
** 数据包的类型
** 视频数据被压缩之后被打包成数据包在网上传输
** 有两种类型的数据包:视频信息包(sps、pps等)和视频数据包(视频的压缩数据)
*/
int nAVCPacketType = pd[1];
int nCompositionTime = CFlvParser::ShowU24(pd + 2);
// 如果是视频配置信息
if (nAVCPacketType == 0) // AVC sequence header
{
ParseH264Configuration(pParser, pd);
}
// 如果是视频数据
else if (nAVCPacketType == 1) // AVC NALU
{
ParseNalu(pParser, pd);
}
else
{
}
return 1;
}
解析视频配置信息
流程如下:
- 解析配置信息的⻓度
- 解析sps、pps的⻓度
- 保存元数据,元数据即sps、pps等
int CFlvParser::CVideoTag::ParseH264Configuration(CFlvParser *pParser, uint8_t *pTagData)
{
uint8_t *pd = pTagData;
// 跨过 Tag Data的VIDEODATA(1字节) AVCVIDEOPACKET(AVCPacketType(1字节) 和CompositionTime(3字节) 4字节)
// 总共跨过5个字节
// NalUnit长度表示占用的字节
pParser->_nNalUnitLength = (pd[9] & 0x03) + 1; // lengthSizeMinusOne 9 = 5 + 4
int sps_size, pps_size;
// sps(序列参数集)的长度
sps_size = CFlvParser::ShowU16(pd + 11); // sequenceParameterSetLength 11 = 5 + 6
// pps(图像参数集)的长度
pps_size = CFlvParser::ShowU16(pd + 11 + (2 + sps_size) + 1); // pictureParameterSetLength
// 元数据的长度
_nMediaLen = 4 + sps_size + 4 + pps_size; // 添加start code
_pMedia = new uint8_t[_nMediaLen];
// 保存元数据
memcpy(_pMedia, &nH264StartCode, 4);
memcpy(_pMedia + 4, pd + 11 + 2, sps_size);
memcpy(_pMedia + 4 + sps_size, &nH264StartCode, 4);
memcpy(_pMedia + 4 + sps_size + 4, pd + 11 + 2 + sps_size + 2 + 1, pps_size);
return 1;
}
解析视频数据
流程如下:
- 如果⼀个Tag还没解析完成,那么执⾏下⾯步骤
- 计算NALU的⻓度
- 获取NALU的起始码
- 保存NALU的数据
- 调⽤⾃定义的处理函数对NALU数据进⾏处理
int CFlvParser::CVideoTag::ParseNalu(CFlvParser *pParser, uint8_t *pTagData)
{
uint8_t *pd = pTagData;
int nOffset = 0;
_pMedia = new uint8_t[_header.nDataSize+10];
_nMediaLen = 0;
// 跨过 Tag Data的VIDEODATA(1字节) AVCVIDEOPACKET(AVCPacketType和CompositionTime 4字节)
nOffset = 5; // 总共跨过5个字节 132 - 5 = 127 = _nNalUnitLength(4字节) + NALU(123字节)
// startcode(4字节) + NALU(123字节) = 127
while (1)
{
// 如果解析完了一个Tag,那么就跳出循环
if (nOffset >= _header.nDataSize)
break;
// 计算NALU(视频数据被包装成NALU在网上传输)的长度,
// 一个tag可能包含多个nalu, 所以每个nalu前面有NalUnitLength字节表示每个nalu的长度
int nNaluLen;
switch (pParser->_nNalUnitLength)
{
case 4:
nNaluLen = CFlvParser::ShowU32(pd + nOffset);
break;
case 3:
nNaluLen = CFlvParser::ShowU24(pd + nOffset);
break;
case 2:
nNaluLen = CFlvParser::ShowU16(pd + nOffset);
break;
default:
nNaluLen = CFlvParser::ShowU8(pd + nOffset);
}
// 获取NALU的起始码
memcpy(_pMedia + _nMediaLen, &nH264StartCode, 4);
// 复制NALU的数据
memcpy(_pMedia + _nMediaLen + 4, pd + nOffset + pParser->_nNalUnitLength, nNaluLen);
// 解析NALU
// pParser->_vjj->Process(_pMedia+_nMediaLen, 4+nNaluLen, _header.nTotalTS);
_nMediaLen += (4 + nNaluLen);
nOffset += (pParser->_nNalUnitLength + nNaluLen);
}
return 1;
}
自定义的视频处理
把视频的NALU解析出来之后,可以根据⾃⼰的需要往视频中添加内容
// 用户可以根据自己的需要,对该函数进行修改或者扩展
// 下面这个函数的功能大致就是往视频中写入SEI信息
int CVideojj::Process(uint8_t *pNalu, int nNaluLen, int nTimeStamp)
{
// 如果起始码后面的两个字节是0x05或者0x06,那么表示IDR图像或者SEI信息
if (pNalu[4] != 0x06 || pNalu[5] != 0x05)
return 0;
uint8_t *p = pNalu + 4 + 2;
while (*p++ == 0xff);
const char *szVideojjUUID = "VideojjLeonUUID";
char *pp = (char *)p;
for (int i = 0; i < strlen(szVideojjUUID); i++)
{
if (pp[i] != szVideojjUUID[i])
return 0;
}
VjjSEI sei;
sei.nTimeStamp = nTimeStamp;
sei.nLen = nNaluLen - (pp - (char *)pNalu) - 16 - 1;
sei.szUD = new char[sei.nLen];
memcpy(sei.szUD, pp + 16, sei.nLen);
_vVjjSEI.push_back(sei);
return 1;
}
音频标签
入口函数CreateTag
创建音频标签
流程如下:
- 初始化
- 解析⾳频编码类型
- 解析采样率
- 解析精度和类型
- 解析⾳频标签
CFlvParser::CAudioTag::CAudioTag(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen, CFlvParser *pParser)
{
Init(pHeader, pBuf, nLeftLen);
uint8_t *pd = _pTagData;
_nSoundFormat = (pd[0] & 0xf0) >> 4; // 音频格式
_nSoundRate = (pd[0] & 0x0c) >> 2; // 采样率
_nSoundSize = (pd[0] & 0x02) >> 1; // 采样精度
_nSoundType = (pd[0] & 0x01); // 是否立体声
if (_nSoundFormat == 10) // AAC
{
ParseAACTag(pParser);
}
}
解析音频标签
流程如下:
- 获取数据包的类型
- 判断数据包的类型
- 如果数据包是⾳频配置信息,那么解析有⾳频配置信息
- 如果是原始⾳频数据,那么对原始⾳频数据进⾏处理
int CFlvParser::CAudioTag::ParseAACTag(CFlvParser *pParser)
{
uint8_t *pd = _pTagData;
// 数据包的类型:音频配置信息,音频数据
int nAACPacketType = pd[1];
// 如果是音频配置信息
if (nAACPacketType == 0) // AAC sequence header
{
// 解析配置信息
ParseAudioSpecificConfig(pParser, pd); // 解析AudioSpecificConfig
}
// 如果是音频数据
else if (nAACPacketType == 1) // AAC RAW
{
// 解析音频数据
ParseRawAAC(pParser, pd);
}
else
{
}
return 1;
}
处理始音频配置
流程如下:
- 解析AAC的采样率
- 解析采样率索引
- 解析声道
int CFlvParser::CAudioTag::ParseAudioSpecificConfig(CFlvParser *pParser, uint8_t *pTagData)
{
uint8_t *pd = _pTagData;
_aacProfile = ((pd[2]&0xf8)>>3); // 5bit AAC编码级别
_sampleRateIndex = ((pd[2]&0x07)<<1) | (pd[3]>>7); // 4bit 真正的采样率索引
_channelConfig = (pd[3]>>3) & 0x0f; // 4bit 通道数量
printf("----- AAC ------\n");
printf("profile:%d\n", _aacProfile);
printf("sample rate index:%d\n", _sampleRateIndex);
printf("channel config:%d\n", _channelConfig);
_pMedia = NULL;
_nMediaLen = 0;
return 1;
}
处理原始音频数据
主要的功能是为原始的⾳频数据添加元数据,可以根据⾃⼰的需要进行改写、
int CFlvParser::CAudioTag::ParseRawAAC(CFlvParser *pParser, uint8_t *pTagData)
{
uint64_t bits = 0; // 占用8字节
// 数据长度 跳过tag data的第一个第二字节
int dataSize = _header.nDataSize - 2; // 减去两字节的 audio tag data信息部分
// 制作元数据
WriteU64(bits, 12, 0xFFF);
WriteU64(bits, 1, 0);
WriteU64(bits, 2, 0);
WriteU64(bits, 1, 1);
WriteU64(bits, 2, _aacProfile - 1);
WriteU64(bits, 4, _sampleRateIndex);
WriteU64(bits, 1, 0);
WriteU64(bits, 3, _channelConfig);
WriteU64(bits, 1, 0);
WriteU64(bits, 1, 0);
WriteU64(bits, 1, 0);
WriteU64(bits, 1, 0);
WriteU64(bits, 13, 7 + dataSize);
WriteU64(bits, 11, 0x7FF);
WriteU64(bits, 2, 0);
// WriteU64执行为上述的操作,最高的8bit还没有被移位到,实际是使用7个字节
_nMediaLen = 7 + dataSize;
_pMedia = new uint8_t[_nMediaLen];
uint8_t p64[8];
p64[0] = (uint8_t)(bits >> 56); // 是bits的最高8bit,实际为0
p64[1] = (uint8_t)(bits >> 48); // 才是ADTS起始头 0xfff的高8bit
p64[2] = (uint8_t)(bits >> 40);
p64[3] = (uint8_t)(bits >> 32);
p64[4] = (uint8_t)(bits >> 24);
p64[5] = (uint8_t)(bits >> 16);
p64[6] = (uint8_t)(bits >> 8);
p64[7] = (uint8_t)(bits);
memcpy(_pMedia, p64+1, 7); // ADTS header, p64+1是从ADTS起始头开始
memcpy(_pMedia + 7, pTagData + 2, dataSize); // AAC body
return 1;
}
其他标签
解析普通标签
/**
* @brief 复制header + body
* @param pHeader
* @param pBuf
* @param nLeftLen
*/
void CFlvParser::Tag::Init(TagHeader *pHeader, uint8_t *pBuf, int nLeftLen)
{
memcpy(&_header, pHeader, sizeof(TagHeader));
// 复制标签头部信息 header
_pTagHeader = new uint8_t[11];
memcpy(_pTagHeader, pBuf, 11); // 头部
// 复制标签 body
_pTagData = new uint8_t[_header.nDataSize];
memcpy(_pTagData, pBuf + 11, _header.nDataSize);
}
