GB28181学习之路——PS流解析H264

4 篇文章 2 订阅
3 篇文章 0 订阅

磕磕绊绊的做了出来,也算为自己留个资料吧。先讲理论再上代码。挑些重点讲。

1. 首先就是获取到 rtp 包,rtp包的结构是:rtp包头+payload,payload就是我们要的ps包,rtp包头的长度是12个字节,所以rtp包去掉前12字节就是ps包了。

比如这个 rtp 包,跳过12个字节,从00 00 01 ba 开始就是ps包了。

2. 找到ps包之后就要从它的格式入手开始解析,ps荷载h264是把一帧帧的数据打包传过来,一个完整的ps包会包含一帧的数据。

而h264的帧分为 i 帧和 p 帧,i 帧的结构是 ps header + ps system header + ps system map + pes + h264 (+ pes + 音频)。

p 帧的结构为 ps header + pes + h264 (+ pes + 音频)。

3. 首先我们要找到第一个 i 帧,找到ps头 00 00 01 ba,ps头长度为14个字节,最后一个字节的后三位的数字标识扩展字节,

这里最后一个字节是 fe ,所以跳过6个字节,到00 00 01 bb。

4. 00 00 01 bb标识 ps system header,它后面的两个字节共同表示ps system header的长度,注意是表示这之后的数据长度,

比如这里ps system header 表示长度的两个字节是 00 12,换算成十进制,就是后面还有18个字节,跳过这18个字节就到了 00 00 01 bc。

5. 00 00 01 bc表示ps system map,它后面的两个字节代表ps system map 的长度。也是这之后的数据长度。

比如这个长度是 00 5e,跳过这 94 个字节就到了 00 00 01 e0,

6,00 00 01 e0就是我们要找的pes 数据包,它后面的两个字节表示pes包剩余数据的长度,跳过两个字节的下一个字节代码pes包的扩展长度。

比如这里 00 22表示后面还有 34个字节,08表示有8个扩展字节,扩展字节之后就是h264的数据,h264数据的长度为pes的长度减去到扩展字节的长度再减去扩展字节。也就是34-3-8 = 23个字节。

如图我们就取到了一个h264的数据了。看到后面又是00 00 01 e0开始的,所以又是一个视频帧。循环往复即可。

下面上代码,这里我使用的是 jrtp,方便获取 rtp 包,测试过海康和大华的两款设备都是没问题的。

代码已上传CSDN:https://download.csdn.net/download/qq_39805297/12566110

MyRTPSession.h

#ifndef MYRTPSESSION_H
#define MYRTPSESSION_H

#include <jrtplib3/rtpsession.h>
#include <jrtplib3/rtppacket.h>
#include <jrtplib3/rtpudpv4transmitter.h>
#include <jrtplib3/rtpipv4address.h>
#include <jrtplib3/rtpsessionparams.h>
#include <jrtplib3/rtperrors.h>
#include <jrtplib3/rtpsourcedata.h>
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <string>

using namespace jrtplib;
//
// The new class routine
//

class MyRTPSession : public RTPSession
{
protected:
	void OnNewSource(RTPSourceData *dat)
	{
		if (dat->IsOwnSSRC())
			return;

		uint32_t ip;
		uint16_t port;

		if (dat->GetRTPDataAddress() != 0)
		{
			const RTPIPv4Address *addr = (const RTPIPv4Address *)(dat->GetRTPDataAddress());
			ip = addr->GetIP();
			port = addr->GetPort();
		}
		else if (dat->GetRTCPDataAddress() != 0)
		{
			const RTPIPv4Address *addr = (const RTPIPv4Address *)(dat->GetRTCPDataAddress());
			ip = addr->GetIP();
			port = addr->GetPort() - 1;
		}
		else
			return;

		RTPIPv4Address dest(ip, port);
		AddDestination(dest);

		struct in_addr inaddr;
		inaddr.s_addr = htonl(ip);
		std::cout << "Adding destination " << std::string(inet_ntoa(inaddr)) << ":" << port << std::endl;
	}

	void OnBYEPacket(RTPSourceData *dat)
	{
		if (dat->IsOwnSSRC())
			return;

		uint32_t ip;
		uint16_t port;

		if (dat->GetRTPDataAddress() != 0)
		{
			const RTPIPv4Address *addr = (const RTPIPv4Address *)(dat->GetRTPDataAddress());
			ip = addr->GetIP();
			port = addr->GetPort();
		}
		else if (dat->GetRTCPDataAddress() != 0)
		{
			const RTPIPv4Address *addr = (const RTPIPv4Address *)(dat->GetRTCPDataAddress());
			ip = addr->GetIP();
			port = addr->GetPort() - 1;
		}
		else
			return;

		RTPIPv4Address dest(ip, port);
		DeleteDestination(dest);

		struct in_addr inaddr;
		inaddr.s_addr = htonl(ip);
		std::cout << "Deleting destination " << std::string(inet_ntoa(inaddr)) << ":" << port << std::endl;
	}

	void OnRemoveSource(RTPSourceData *dat)
	{
		if (dat->IsOwnSSRC())
			return;
		if (dat->ReceivedBYE())
			return;

		uint32_t ip;
		uint16_t port;

		if (dat->GetRTPDataAddress() != 0)
		{
			const RTPIPv4Address *addr = (const RTPIPv4Address *)(dat->GetRTPDataAddress());
			ip = addr->GetIP();
			port = addr->GetPort();
		}
		else if (dat->GetRTCPDataAddress() != 0)
		{
			const RTPIPv4Address *addr = (const RTPIPv4Address *)(dat->GetRTCPDataAddress());
			ip = addr->GetIP();
			port = addr->GetPort() - 1;
		}
		else
			return;

		RTPIPv4Address dest(ip, port);
		DeleteDestination(dest);

		struct in_addr inaddr;
		inaddr.s_addr = htonl(ip);
		std::cout << "Deleting destination " << std::string(inet_ntoa(inaddr)) << ":" << port << std::endl;
	}
};

#endif //MYRTPSESSION_H

main.cpp

#include "myrtpsession.h"

#define PS_BUFF_SIZE 4096000
#define SAVE_PS_FILE 1
#define SAVE_H264_FILE 1

//
// This function checks if there was a RTP error. If so, it displays an error
// message and exists.
//

uint8_t *_frameBuff;
int _frameSize;
int _buffLen;
FILE* fp_h264;

void checkerror(int rtperr)
{
	if (rtperr < 0)
	{
		std::cout << "ERROR: " << RTPGetErrorString(rtperr) << std::endl;
		exit(-1);
	}
}

void writeH264Frame()
{
	printf("write frame size=%d\n", _buffLen);
	if (_frameSize != _buffLen)
		printf("error:frameSize=%d bufflen=%d\n", _frameSize, _buffLen);
#if SAVE_H264_FILE
	fwrite(_frameBuff, 1, _buffLen, fp_h264);
#endif
	memset(_frameBuff, 0, sizeof(_frameBuff));
	_buffLen = 0;
	_frameSize = 0;
}

void getH264Frame(uint8_t* payloadData, int payloadLength)
{
	int pos = 0;
	memset(_frameBuff, 0, sizeof(_frameBuff));
	_buffLen = 0;
	 _frameSize = 0;
	/******  统一 帧  ******/
	while (payloadData[pos] == 0x00 && payloadData[pos+1] == 0x00 && 
        payloadData[pos+2] == 0x01 && payloadData[pos+3] == 0xe0)
	{
		uint16_t h264_size = payloadData[pos+4] << 8 | payloadData[pos+5];
		uint8_t expand_size = payloadData[pos+8];
		_frameSize = h264_size - 3 - expand_size;
		pos += 9 + expand_size;
		//全部写入并保存帧
		if (_frameSize <= payloadLength - pos)
		{
			{
				memcpy(_frameBuff, payloadData + pos, _frameSize);
				_buffLen += _frameSize;
				pos += _frameSize;
				writeH264Frame();
			}
			if (pos >= payloadLength)
				break;
		}
		else
		{
			memcpy(_frameBuff, payloadData + pos, payloadLength - pos);
			_buffLen += payloadLength - pos;
			printf("Frame size:%d\n", _frameSize);
			break;
		}
	}
}

void getH264FromPacket(uint8_t* payloadData, int payloadLength)
{
	int pos = 0;
    uint8_t expand_size = payloadData[13] & 0x07;//扩展字节
    pos += 14 + expand_size;//ps包头14
    /******  i 帧  ******/
    if (payloadData[pos] == 0x00 && payloadData[pos + 1] == 0x00 && 
        payloadData[pos + 2] == 0x01 && payloadData[pos + 3] == 0xbb)
    {//ps system header
	    uint16_t psh_size = payloadData[pos + 4] << 8 | payloadData[pos + 5];//psh长度
	    pos += 6 + psh_size;
	    if (payloadData[pos] == 0x00 && payloadData[pos + 1] == 0x00 && 
            payloadData[pos + 2] == 0x01 && payloadData[pos + 3] == 0xbc)
	    {//ps system map
		    uint16_t psm_size = payloadData[pos + 4] << 8 | payloadData[pos + 5];
		    pos += 6 + psm_size;
		}
	    else
	    {
		    printf("no system map and no video stream\n");
		    return;
	    }
	}
    getH264Frame(payloadData + pos, payloadLength - pos);
}


void executeProcess(int port, int secs)
{
#ifdef RTP_SOCKETTYPE_WINSOCK
	WSADATA dat;
	WSAStartup(MAKEWORD(2, 2), &dat);
#endif // RTP_SOCKETTYPE_WINSOCK

#if SAVE_PS_FILE
	FILE* fp_ps = fopen("gb.ps", "w");
#endif // SAVE_PS_FILE
#if SAVE_H264_FILE
	fp_h264 = fopen("gb.h264", "w");
#endif // SAVE_H264_FILE

	MyRTPSession sess;
	std::string ipstr;
	int status, j;

	// Now, we'll create a RTP session, set the destination
	// and poll for incoming data.

	RTPUDPv4TransmissionParams transparams;
	RTPSessionParams sessparams;

	// IMPORTANT: The local timestamp unit MUST be set, otherwise
	//            RTCP Sender Report info will be calculated wrong
	// In this case, we'll be just use 8000 samples per second.
	sessparams.SetOwnTimestampUnit(1.0 / 8000.0);

	sessparams.SetAcceptOwnPackets(true);
	transparams.SetPortbase(port);
	status = sess.Create(sessparams, &transparams);
	checkerror(status);
	_frameBuff = new uint8_t[PS_BUFF_SIZE];
	memset(_frameBuff, 0, sizeof(_frameBuff));
	_frameSize = 0;
	_buffLen = 0;

	for (j = 1; j <= secs; j++)
	{
		sess.BeginDataAccess();
		printf("secs gone %d\n", j);
		// check incoming packets
		if (sess.GotoFirstSourceWithData())
		{
			do
			{
				RTPPacket *pack;
				while ((pack = sess.GetNextPacket()) != NULL)
				{
					printf("Got packet\n");
					// You can examine the data here
					if (pack->GetPayloadType() == 96)
					{
#if SAVE_PS_FILE
						fwrite(pack->GetPayloadData(), 1, pack->GetPayloadLength(), fp_ps);
#endif
						//查找ps头 0x000001BA
						if (pack->GetPayloadData()[0] == 0x00 && pack->GetPayloadData()[1] == 0x00 &&
							pack->GetPayloadData()[2] == 0x01 && pack->GetPayloadData()[3] == 0xba)
						{
							getH264FromPacket(pack->GetPayloadData(), pack->GetPayloadLength());
						}
                        else if (pack->GetPayloadData()[0] == 0x00 && pack->GetPayloadData()[1] == 0x00 &&
							pack->GetPayloadData()[2] == 0x01 && pack->GetPayloadData()[3] == 0xe0)
                        {
                            getH264Frame(pack->GetPayloadData(), pack->GetPayloadLength());
                        }
						else  //当然如果开头不是0x000001BA,默认为一个帧的中间部分,我们将这部分内存顺着帧的开头向后存储
						{
							//排除音频和私有数据
							if (pack->GetPayloadData()[0] == 0x00 && pack->GetPayloadData()[1] == 0x00 &&
								pack->GetPayloadData()[2] == 0x01 && pack->GetPayloadData()[3] == 0xc0)
							{ 
							}
							else if (pack->GetPayloadData()[0] == 0x00 && pack->GetPayloadData()[1] == 0x00 &&
								pack->GetPayloadData()[2] == 0x01 && pack->GetPayloadData()[3] == 0xbd)
							{ 
							}
							else   //这是正常的帧数据,像贪吃蛇一样,将它放在帧开头的后边
							{
								if (pack->GetPayloadLength() + _buffLen >= _frameSize)
								{
									int len = _frameSize - _buffLen;
									memcpy(_frameBuff + _buffLen, pack->GetPayloadData(), len);
									_buffLen += len;
									writeH264Frame();
									if (pack->GetPayloadLength() > len)
										getH264FromPacket(pack->GetPacketData() + len, pack->GetPacketLength() - len);
								}
								else
								{
									memcpy(_frameBuff + _buffLen, pack->GetPayloadData(), pack->GetPayloadLength());
									_buffLen += pack->GetPayloadLength();
								}
							}
						}
					}
					// we don't longer need the packet, so
					// we'll delete it
					sess.DeletePacket(pack);
				}
			} while (sess.GotoNextSourceWithData());
		}

		sess.EndDataAccess();

#ifndef RTP_SUPPORT_THREAD
		status = sess.Poll();
		checkerror(status);
#endif // RTP_SUPPORT_THREAD

		RTPTime::Wait(RTPTime(1, 0));
	}

	sess.BYEDestroy(RTPTime(10, 0), 0, 0);

#ifdef RTP_SOCKETTYPE_WINSOCK
	WSACleanup();
#endif // RTP_SOCKETTYPE_WINSOCK
#if SAVE_PS_FILE
	fclose(fp_ps);
#endif
#if SAVE_H264_FILE
	fclose(fp_h264);
#endif
	printf("StreamReciever exits\n");
}

int main()
{
    executeProcess(19444, 100);
    return 0;
}

 

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Spark是一种大数据处理的框架,它可以处理大量的数据并进行分析。初学者可以通过学习Spark的基本概念和使用方法,了解Spark的工作原理和应用场景。在学习Spark的过程中,需要掌握Spark的核心组件和API,例如Spark Core、Spark SQL、Spark Streaming等。此外,还需要学习Spark的部署和调优,以及与其他大数据技术的集成。 ### 回答2: Spark是一种基于内存的分布式计算框架,是大数据处理中最行的技术之一。Spark简单易用,能够快速地处理海量数据,尤其是在机器学习和数据挖掘领域中表现突出。本文将从初识Spark的角度入手,介绍Spark的基本概念和使用。 一、Spark的基本概念 1. RDD RDD全称为Resilient Distributed Datasets,中文意思是弹性分布式数据集,它是Spark的核心数据结构。RDD是一个不可变的分布式的对象集合,可以跨越多个节点进行并行处理。一个RDD可以分为多个分区,每个分区可以在不同的节点上存储。 2. DAG DAG即Directed Acyclic Graph(有向无环图),它是Spark中的一个概念,用来表示作业的依赖关系。Spark将一个作业拆分成一系列具有依赖关系的任务,每个任务之间的依赖形成了DAG。 3. 窄依赖和宽依赖 对于一个RDD,如果一个子RDD的每个分区只依赖于父RDD的一个分区,这种依赖就称为窄依赖。如果一个子RDD的每个分区依赖于父RDD的多个分区,这种依赖就称为宽依赖。宽依赖会影响Spark的性能,应尽量避免。 二、Spark的使用 1. 安装Spark 要使用Spark,首先需要在本地或者集群上安装Spark。下载安装包解压缩即可,然后设置环境变量,即可在命令行中运行Spark。 2. Spark Shell Spark Shell是Spark的交互式命令行界面,类似于Python的交互式控制台,可以快速测试Spark代码。在命令行中输入spark-shell即可进入。 3. Spark应用程序 除了Spark Shell,Spark还支持以应用程序的形式运行。要创建一个Spark应用程序,可以使用Scala、Java、Python等语言进行编写。使用Spark API,读取数据、处理数据、保存数据等操作都可以通过编写代码完成。 总之,Spark是一种优秀的分布式计算框架,能够在海量数据处理中发挥出强大的作用。初学者可以从掌握RDD、DAG、依赖关系等基本概念开始,逐步深入学习Spark的使用。 ### 回答3: Spark是一种快速、分布式数据处理框架,它能够在成千上万个计算节点之间分配数据和计算任务。Spark的优势在于它支持多种语言和数据源,可以在内存中快速存储和处理数据。 在初学Spark时,我们需要对Spark的架构和核心组件有一些了解。首先,Spark的核心组件是Spark Core,它是一个可以用于建立各种应用程序的计算引擎。与此同时,Spark持有丰富的库,包括Spark SQL、Spark Streaming、MLLib和GraphX等,以支持在各种数据类型(文本、图像、视频、地理定位数据等)上运行各种算法。 若想要在Spark中进行任务,有两种编程API可供选择:Spark的核心API和Spark的SQL及DataFrame API。Spark的核心API基于RDDs(弹性分布式数据集),它是不可变的分布式对象集合,Spark使用RDD来处理、缓存和共享数据。此外,Spark的SQL及DataFrame API提供了更高层次的语言,可以处理结构化和半结构化数据。 除了组件和API之外,我们还需要了解Spark的4个运行模式:本地模式、Standalone模式、YARN模式和Mesos模式。本地模式由单个JVM上单个线程(本地模式)或四个线程(local[*]模式)运行。Standalone通常用于小规模集群或开发和测试环境。在YARN或Mesos模式下,Spark将任务提交给集群管理器,并通过管理器分配和管理资源。 总体来说,初学Spark时,我们需要了解Spark的核心组件、编程API和运行模式。熟悉这些概念以及Spark的架构,可以帮助我们更好地理解Spark和构建高效且可扩展的Spark应用程序。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值