H264和音频流打包成PS流 (MPEG2-PS)
技术在于交流、沟通,转载请注明出处并保持作品的完整性。
原文:https://blog.csdn.net/hiwubihe/article/details/80736848
[本系列相关文章]
- H264和音频流打包成PS流 (MPEG2-PS)
- PS流解复用成H264和音频流(ES提取)
- H264和音频流打包成TS流 (MPEG2-TS)
- TS流解复用成H264和音频流(ES提取)
- FLV文件格式基础
- 解复用FLV文件(基于FFMPEG解析FLV(h264+aac))
- 解复用FLV文件(不用FFMPEG,C++实现)
在诸如安防系统标准GB28181和“电网视频监控系统及接口”中,固定视频流的传输格式为PS格式。PS流和TS流的概念是在MPEG2的ISO/IEC-13818标准的第一部分“系统”中提出的。其提出的目的是提供MPEG2编码比特的存储与传输方案。TS流在后面博文中阐述,本文将基于C/C++提供一个PS流的打包库PsMuxer.dll,并提供DEMO测试程序。打包程序包括一个PsMuxerDemo和PsMuxer库,文档包括主要参考的几篇文章"iso13818-1.pdf" "PS流和TS流介绍.docx","视音频数据PS封装-offset.doc"。
先介绍PS封装中的基本元素
- ES 音视频采集编码后等待打包PS的单元,H264就是一个I帧,P帧或者其他H264辅助信息。音频的打包方式都是一样的,目前PsMuxer.dll只支持H264和各种音频的ES流,本来打算添加H265和MPEG4-PART2的,考虑到H265并不是iso13818-1规定的类型,MPEG4-PART2打包和H264基本相同,H265只是标准扩展的,所以厂商实现不统一,就去掉这部分内容,后期需要实现可以添加。标准支持情况在Table 2-29中有表述
- PES 编码层之上的传输或者存储单元,一个PS包里面可以有多个PES包,这些PES可以是一路流的多个单元(如I帧太大超过标准的65535,只能分包),也可以是多路流的多个单元,如PS包中包含一个PES视频包和一个PES音频包。解复用PS流主要只有PES包里面有ES。PES头包括类别码,和PTS,DTS信息。
- PS包头 表示一个PS包的开始。一般来说一个节目流PS只包括一个节目流,一个节目流中有多路流,如视频流1,视频流2,音频流,解复用PS流主要依据每一路流stream_id区别。解复用把不同stream_id的流分别做不同处理,如在不同屏幕显示等。PS头部主要包括比特率,注意是以50B为单位的,PsMuxer.dll中处理方式为统计一段时间数据流量,然后填写,过一段时间重新统计下一段时间区间的平均比特率。PS头部还包括时钟参考信息,为当前PS段最后1B数据期望送入解码器的时间,和PES中的PTS不一样,但是PsMuxer.dll中简单处理,把视频流中PTS放入PS头部时钟参考,时间以90KHZ为单位。
- PS系统头,系统头 主要包括 头长度,视频流个数,音频流个数,及每一路流信息列举(stream_id,视频还是音频),stream_id怎么分配在下面讲解。
- PS MAP头 主要包括PS节目流包含的流信息,流类型,stream_id,流描述信息。这里的流类型很重要,解码器需要根据流类型解码,H264码流需要赋值PSMUX_ST_VIDEO_H264,如果赋值成PSMUX_ST_VIDEO_MPEG4,交给VLC是播放不了的。
PS支持复用音视频类型
//流类型 /* Table 2-29 in spec */
enum PsMuxStreamType
{
PSMUX_ST_RESERVED = 0x00,
PSMUX_ST_VIDEO_MPEG1 = 0x01,
PSMUX_ST_VIDEO_MPEG2 = 0x02,
PSMUX_ST_AUDIO_MPEG1 = 0x03,
PSMUX_ST_AUDIO_MPEG2 = 0x04,
PSMUX_ST_PRIVATE_SECTIONS = 0x05,
PSMUX_ST_PRIVATE_DATA = 0x06,
PSMUX_ST_MHEG = 0x07,
PSMUX_ST_DSMCC = 0x08,
PSMUX_ST_H222_1 = 0x09,
/* later extensions */
PSMUX_ST_AUDIO_AAC = 0x0f,
PSMUX_ST_VIDEO_MPEG4 = 0x10,
PSMUX_ST_VIDEO_H264 = 0x1b,
/* private stream types */
PSMUX_ST_PS_AUDIO_AC3 = 0x81,
PSMUX_ST_PS_AUDIO_DTS = 0x8a,
PSMUX_ST_PS_AUDIO_LPCM = 0x8b,
PSMUX_ST_PS_AUDIO_G711A = 0x90,
PSMUX_ST_PS_AUDIO_G711U = 0x91,
PSMUX_ST_PS_AUDIO_G722_1 = 0x92,
PSMUX_ST_PS_AUDIO_G723_1 = 0x93,
PSMUX_ST_PS_AUDIO_G729 = 0x99,
PSMUX_ST_PS_AUDIO_SVAC = 0x9b,
PSMUX_ST_PS_DVD_SUBPICTURE = 0xff,
//下面定义不是标准里面定义的
/* Non-standard definitions */
PSMUX_ST_VIDEO_DIRAC = 0xD1
};
PS流中stream_id怎么分配的,主要依据类型,如视频流从0XE0分配到0XEF,最多可以包含16路视频流,具体定义如下:
//视频流stream_id分配初始化值
#define PSMUX_STREAM_ID_MPGA_INIT 0xc0
//视频流stream_id分配最大值
#define PSMUX_STREAM_ID_MPGA_MAX 0xcf
//音频流stream_id分配初始化值
#define PSMUX_STREAM_ID_MPGV_INIT 0xe0
//音频流stream_id分配最大值
#define PSMUX_STREAM_ID_MPGV_MAX 0xef
#define PSMUX_STREAM_ID_AC3_INIT 0x80
#define PSMUX_STREAM_ID_AC3_MAX 0x87
#define PSMUX_STREAM_ID_SPU_INIT 0x20
#define PSMUX_STREAM_ID_SPU_MAX 0x3f
#define PSMUX_STREAM_ID_DTS_INIT 0x88
#define PSMUX_STREAM_ID_DTS_MAX 0x8f
#define PSMUX_STREAM_ID_LPCM_INIT 0xa0
#define PSMUX_STREAM_ID_LPCM_MAX 0xaf
#define PSMUX_STREAM_ID_DIRAC_INIT 0x60
#define PSMUX_STREAM_ID_DIRAC_MAX 0x6f
PS封装H264流程
I帧封装 PS header | PS system header | PS system Map | PES header | h264 raw data,每个IDR NALU前一般都会包含SPS、PPS等NALU,因此将SPS、PPS、IDR 的NALU封装为一个PS包,包括ps头,然后加上PS system header,PS system map,PES header+h264 rawdata
P帧封装 PS header | PES header | h264 raw data 非关键帧的PS包,直接加上PS头和PES头就可以了
音频封装 当有音频数据时,将数据加上PES header 放到视频PES后就可以了。顺序如下:PS包=PS头|PES(video)|PES(audio)
PS封装有不懂可以参照上面文档介绍。
PS复用库PsMuxer.dll的头文件
/*******************************************************************************
Copyright (c) wubihe Tech. Co., Ltd. All rights reserved.
--------------------------------------------------------------------------------
Date Created: 2014-10-25
Author: wubihe
Description: PS流封装库头文件
--------------------------------------------------------------------------------
Modification History
DATE AUTHOR DESCRIPTION
--------------------------------------------------------------------------------
********************************************************************************/
#ifndef IPSMUXER_H_
#define IPSMUXER_H_
#ifdef WIN32
#include <windows.h>
#include <windef.h>
#ifdef PSMUXER_EXPORTS
#define DLLEXPORT __declspec(dllexport)
//#define DLLEXPORT
#else
#define DLLEXPORT
#endif
#else
#define DLLEXPORT
#define WINAPI
#endif //WIN32
#include <string>
///////////////////////////////////////////////////////////////////////////
#ifdef __cplusplus
extern "C"
{
#endif
/******************************************************************************
PsMuxer.dll宏定义
*******************************************************************************/
/******************************************************************************
PsMuxer.dll错误码定义,HPsMuxer.dll库错误码的范围:0-255
*******************************************************************************/
/******************************************************************************
HPlayer.dll数据结构定义
*******************************************************************************/
///日志级别类型
typedef enum _PM_LOG_LEVEL
{
PM_LOG_TRACE = 0,
PM_LOG_DEBUG = 1,
PM_LOG_INFO = 2,
PM_LOG_WARN = 3,
PM_LOG_ERROR = 4,
PM_LOG_FATAL = 5
} PM_LOG_LEVEL;
//复合流类型
typedef enum _PM_STREAM_TYPE
{
MUXSER_VIDEO_TYPE_H264 = 0,
MUXSER_VIDEO_TYPE_H265 = 1,
MUXSER_AUDIO_TYPE_G711A = 2,
MUXSER_AUDIO_TYPE_AAC = 3,
MUXSER_SUPPORT_NUM = 4
} PM_STREAM_TYPE;
//帧转换结果类型
typedef enum _PM_RESULT_TYPE
{
//帧换成功
PM_RESULT_OK = 0,
//SPS/PPS/SEI类型数据转换时,转换成功返回此值,但是没有输出信息,必须一直送入IDR才有输出
PM_RESULT_WAIT = 1,
//送入转换器数据非法
PM_RESULT_FRAME = 2,
//转换出错,如缓存不够等
PM_RESULT_ERROR = 3
} PM_RESULT_TYPE;
//帧信息
typedef struct tagPmFrameInfo
{
//帧数据
unsigned char * pFrame ;
//帧大小
int iFrameLen ;
//帧PTS
LONG64 lPts ;
//帧DTS
LONG64 lDts ;
} PmFrameInfo;
/****************************************************************************
General Callback
通用回调接口
****************************************************************************/
typedef void(CALLBACK *PM_LogCBFun)(PM_LOG_LEVEL nLogLevel, const char *szMessage, void* pUserData );
/****************************************************************************
General System Interface
通用系统接口
****************************************************************************/
/**************************************************************************
* Function Name : PM_SetLogCallBack
* Description : 设置库日志回调
* Parameters : pLogFunc (日志回调函数)
* Parameters : pUserData(日志回调用户数据)
* Return Type : void
* Last Modified : wubihe
***************************************************************************/
DLLEXPORT void WINAPI PM_SetLogCallBack(PM_LogCBFun pLogFunc, void* pUserData);
/**************************************************************************
* Function Name : PM_CreateMuxHandle
* Description : 创建PS流复用器句柄
* Parameters :
* Return Type : int >1为合法句柄,<=0非法 最大支持299路
* Last Modified : wubihe
***************************************************************************/
DLLEXPORT int WINAPI PM_CreateMuxHandle();
/**************************************************************************
* Function Name : PM_AddStream
* Description : PS流复用器中添加流
* Parameters : iHandle 复用器句柄
* Parameters : eType 流类型句柄
* Return Type : int 流句柄 >=0 为合法句柄,<0非法 最大支持2路
* Last Modified : wubihe
***************************************************************************/
DLLEXPORT int WINAPI PM_AddStream(int iHandle, PM_STREAM_TYPE eType);
/**************************************************************************
* Function Name : PM_FrameTrans
* Description : PS流复用器复用视频帧
* Parameters : iHandle 复用器句柄
* Parameters : iStream 流句柄
* Parameters : stFrameInfo 帧信息
* Parameters : outBuf 输出缓存
* Parameters : pOutSize 输入为输出缓存大小,输出为实际封装后数据大小
* Return Type : PM_RESULT_TYPE
* Last Modified : wubihe
***************************************************************************/
DLLEXPORT PM_RESULT_TYPE WINAPI PM_FrameTrans(int iHandle, int iStream, PmFrameInfo stFrameInfo,unsigned char * outBuf, int* pOutSize);
/**************************************************************************
* Function Name : PM_DestroyMuxHandle
* Description : PS流复用器销毁
* Parameters : iHandle 复用器句柄
* Return Type : void
* Last Modified : wubihe
***************************************************************************/
DLLEXPORT void WINAPI PM_DestroyMuxHandle(int iHandle);
#ifdef __cplusplus
}
#endif
#endif /* IPSMUXER_H_ */
PS复用库PsMuxer.dll的调用流程
PS复用库PsMuxer.dll调用Demo
/*******************************************************************************
Copyright (c) wubihe Tech. Co., Ltd. All rights reserved.
--------------------------------------------------------------------------------
Date Created: 2014-10-25
Author: wubihe QQ:1269122125 Email:aaa@qq.com
Description: PS流封装库PsMuxer使用Demo 试用版本Demo只能运行240S 需要授权库或者
全部源码请联系作者
--------------------------------------------------------------------------------
Modification History
DATE AUTHOR DESCRIPTION
--------------------------------------------------------------------------------
********************************************************************************/
#include "excpt.h"
#include "TIOBuffer.h"
#include "IPsMuxer.h"
#define MAX_BUFFER_SIZE (1024*8)
#define MAX_OUT_BUFFER_SIZE (1024*1024)
static FILE *gInputFile = NULL;
static FILE *gOutputFile = NULL;
//读取H264数据缓存
unsigned char gszReadBuffer[MAX_BUFFER_SIZE];
//解析h264数据缓存
comn::IOBuffer gH264ParserBuff;
struct NaluPacket
{
unsigned char* data;
int length;
int prefix;
};
//帧类型定义
enum NAL_type
{
NAL_IDR,
NAL_SPS,
NAL_PPS,
NAL_SEI,
NAL_PFRAME,
NAL_VPS,
NAL_SEI_PREFIX,
NAL_SEI_SUFFIX,
NAL_other,
NAL_TYPE_NUM
};
unsigned char* g_pMuxBuf;
long g_Pts = 0;
long g_Dts = 0;
//判断是否是264或者265帧,如果是顺便把NalTypeChar设置一下
bool isH264Or265Frame(unsigned char* buf, unsigned char* NalTypeChar,int *iHeadLen)
{
bool bOk = false;
unsigned char c = 0;
if (buf[0] == 0 && buf[1] == 0 && buf[2] == 0 && buf[3] == 1)
{
if (NalTypeChar)
{
*NalTypeChar = buf[4];
}
*iHeadLen = 4;
bOk = true;
}
if (buf[0] == 0 && buf[1] == 0 && buf[2] == 1 )
{
if (NalTypeChar)
{
*NalTypeChar = buf[3];
}
*iHeadLen = 3;
bOk = true;
}
return bOk;
}
NAL_type getH264NALtype(unsigned char c)
{
switch(c & 0x1f){
case 6:
return NAL_SEI;
break;
case 7:
return NAL_SPS;
break;
case 8:
return NAL_PPS;
break;
case 5:
return NAL_IDR;
break;
case 1:
return NAL_PFRAME;
break;
default:
return NAL_other;
break;
}
return NAL_other;
}
NAL_type getH265NALtype(unsigned char c)
{
int type = (c & 0x7E)>>1;
if(type == 33)
return NAL_SPS;
if(type == 34)
return NAL_PPS;
if(type == 32)
return NAL_VPS;
if(type == 39)
return NAL_SEI_PREFIX;
if(type == 40)
return NAL_SEI_SUFFIX;
if((type >= 1) && (type <=9))
return NAL_PFRAME;
if((type >= 16) && (type <=21))
return NAL_IDR;
return NAL_other;
}
void ProcessData(int iHandle ,int iStream,unsigned char* buf, int len)
{
PmFrameInfo stFrameInfo;
stFrameInfo.pFrame = buf;
stFrameInfo.iFrameLen = len;
stFrameInfo.lPts = g_Pts;
stFrameInfo.lDts = g_Dts;
int MuxOutSize = 0;
PM_RESULT_TYPE eResult;
int iOutSize = MAX_OUT_BUFFER_SIZE;
eResult = PM_FrameTrans(iHandle, iStream, stFrameInfo,g_pMuxBuf, &iOutSize);
if (eResult == PM_RESULT_OK && iOutSize > 0)
{
fwrite(g_pMuxBuf, iOutSize, 1, gOutputFile);
fflush(gOutputFile);
}
else if(eResult == PM_RESULT_WAIT)
{
//printf("mux wait!\n");
}
else
{
printf("mux error !\n");
}
unsigned char c = 0;
int iHeadLen;
if (!isH264Or265Frame(buf, &c,&iHeadLen))
{
return;
}
NAL_type Type = getH264NALtype(c);
if ((Type == NAL_IDR) || (Type == NAL_PFRAME))
{
//Pts递增25fps,0.04s一帧,时间9000HZ为单位,所以3600
g_Pts += 3600;
g_Dts += 3600;
}
}
bool findNalu(unsigned char* buffer, size_t length, size_t start, NaluPacket& packet)
{
__try{
if ((length < 3) || ((length - start) < 3))
{
return false;
}
bool found = false;
unsigned char* p = buffer;
for (size_t i = start; i < (length - 3); ++ i)
{
if ((p[i] == 0) && (p[i+1] == 0))
{
if (p[i+2] == 0)
{
if (((i + 3) < length) && (p[i+3] == 1))
{
//0x 00 00 00 01
packet.data = p + i;
packet.length = i;
packet.prefix = 4;
found = true;
break;
}
}
else if (p[i+2] == 1)
{
packet.data = p + i;
packet.length = i;
packet.prefix = 3;
found = true;
break;
}
}
}
return found;
}__except(EXCEPTION_EXECUTE_HANDLER)
{
return false;
}
}
void show_usage(const char *name)
{
printf("usage:\n");
printf(" for test ps streaming: %s input_file\n", name);
getchar();
}
int main(int argc, char* argv[])
{
if (argc != 2)
{
show_usage(argv[0]);
return 0;
}
char szOutFileName[256] = {0};
sprintf(szOutFileName, "%s.mpeg", argv[1]);
gInputFile = fopen(argv[1], "rb");
if (!gInputFile)
{
printf("read file failed!\n");
return 0;
}
gOutputFile = fopen(szOutFileName, "wb");
if (!gOutputFile)
{
printf("open output file failed!\n");
return 0;
}
g_pMuxBuf = new unsigned char[MAX_OUT_BUFFER_SIZE];
int iMuxHandle = PM_CreateMuxHandle();
if(iMuxHandle <= 0)
{
printf("PM_CreateMuxHandle Error!\n");
return 0;
}
int iStreamId;
iStreamId = PM_AddStream(iMuxHandle, MUXSER_VIDEO_TYPE_H264);
if(iStreamId < 0)
{
printf("PM_AddStream Error!\n");
return 0;
}
int iReadSize = fread(gszReadBuffer, 1, MAX_BUFFER_SIZE, gInputFile);
while(iReadSize > 0)
{
gH264ParserBuff.write(gszReadBuffer, iReadSize);
unsigned char *pBufferDataPtr;
size_t sBufferDataReadable;
while (true)
{
pBufferDataPtr = gH264ParserBuff.getReadPtr();
sBufferDataReadable = gH264ParserBuff.readable();
NaluPacket firstPacket;
if (!findNalu(pBufferDataPtr, sBufferDataReadable, 0, firstPacket))
{
break;
}
NaluPacket secondPacket;
if (!findNalu(pBufferDataPtr, sBufferDataReadable, firstPacket.length + firstPacket.prefix, secondPacket))
{
break;
}
firstPacket.length = secondPacket.length - firstPacket.length;
ProcessData(iMuxHandle ,iStreamId,firstPacket.data, firstPacket.length);
gH264ParserBuff.skip(secondPacket.length);
}
//实际播放应该按照比特率播放
iReadSize = fread(gszReadBuffer, 1, MAX_BUFFER_SIZE, gInputFile);
}
fclose(gInputFile);
fclose(gOutputFile);
printf("Ps file: %s Generate Success!\n",szOutFileName);
printf("GetChar To Exit!\n");
getchar();
return 0;
}
DEMO使用方法
工程目录/bin/目录运行 PsMuxerDemo_T.exe huangdun.264 将生成PS文件huangdun.264.mpeg
运行结果码流分析,Elecard StreamAnalysis没有分析处理PS_MAP。
思考:
音视频流封装格式有很多,但是为什么只有PS或者TS适合网络传输呢?可能有其他原因,但是我理解的有一个原因应该是一般封装格式,你需要特殊的流化处理,这个可以在live555源码里面看到,ps是可以直接切断打包RTP传输的,即使你PS开始部分数据丢失,等到I帧来的时候,你又有PS系统头 PSMAP信息,你就知道当前流有多少stream,分别是什么格式编码,所以能解码播放。
MPEG2-PS对编码方式的支持情况:
编译环境: Win7_64bit+VS2008
DEMO下载地址:https://download.csdn.net/download/hiwubihe/10487109