天天看点

流媒体之RTMP——librtmp拉流测试

文章目录

    • 一:LibRTMP拉流
      • 1.1 拉流保存成FLV
      • 1.2 拉流解析出H264和AAC

作者:一步(Reser)

日期:2019.10.11

一:LibRTMP拉流

1.1 拉流保存成FLV

常见的使用方式是直接拉流成 FLV 文件:

/**
* @brief:
* Test librtmp of pulling streams
*
* Frames from server --> puller --> local file(.flv or .h264 or .aac)
*/
class CTestLibRTMPPuller
{
public:
	CTestLibRTMPPuller();
	virtual ~CTestLibRTMPPuller();

	bool create(const std::string &file);
	void destroy();
	bool connect(const std::string &url, uint32_t timeout_secs);
	void disconnect();

	static void thread_proc(void *param)
	{
		CTestLibRTMPPuller *this_ptr = (CTestLibRTMPPuller *)param;
		if (NULL != this_ptr)
			this_ptr->thread_proc_internal();
	}
	void thread_proc_internal();

protected:
	bool _init_sockets();
	void _cleanup_sockets();

protected:
	RTMP *_rtmp_ptr;
	bool _running;
	std::thread *_thread_ptr;

	uint32_t _buffer_size;
	uint8_t *_buffer_ptr;
	FILE *_file_ptr;
};
           

实现:

CTestLibRTMPPuller::CTestLibRTMPPuller()
{
	_rtmp_ptr = NULL;
	_running = false;
	_thread_ptr = NULL;

	_buffer_size = 0;
	_buffer_ptr = NULL;
	_file_ptr = NULL;
}

CTestLibRTMPPuller::~CTestLibRTMPPuller()
{
}

bool CTestLibRTMPPuller::create(const std::string &file)
{
	bool success = false;

	do {
		if (file.empty())
			break;

		// Init socket
		if (!_init_sockets())
			break;

		// Librtmp init
		_rtmp_ptr = RTMP_Alloc();
		if (NULL == _rtmp_ptr)
			break;
		RTMP_Init(_rtmp_ptr);

		// Recv buffers
		_buffer_size = 2 * 1024 * 1024; // 2MB
		_buffer_ptr = new uint8_t[_buffer_size];
		if (NULL == _buffer_ptr)
			break;
		_file_ptr = fopen(file.c_str(), "wb+");
		if (NULL == _file_ptr)
			break;

		success = true;
	} while (false);

	if (!success) {
		destroy();
	}

	return success;
}

void CTestLibRTMPPuller::destroy()
{
	if (NULL != _rtmp_ptr) {
		RTMP_Free(_rtmp_ptr);
		_rtmp_ptr = NULL;
	}
	if (NULL != _buffer_ptr) {
		delete[] _buffer_ptr;
		_buffer_ptr = NULL;
	}
	_buffer_size = 0;
	if (NULL != _file_ptr) {
		fclose(_file_ptr);
		_file_ptr = NULL;
	}
	_cleanup_sockets();
}

bool CTestLibRTMPPuller::connect(const std::string &url, uint32_t timeout_secs)
{
	bool success = false;

	do {
		// Parse rtmp url
		_rtmp_ptr->Link.timeout = timeout_secs;
		_rtmp_ptr->Link.lFlags |= RTMP_LF_LIVE;
		if (RTMP_SetupURL(_rtmp_ptr, (char *)url.c_str()) < 0)
			break;

		// Set recv buffers
		RTMP_SetBufferMS(_rtmp_ptr, 2 * 1024 * 1024); // 2MB

		// Socket connection
		// Handshakes and connect command
		if (RTMP_Connect(_rtmp_ptr, NULL) < 0)
			break;

		// Setup stream and stream settings
		if (RTMP_ConnectStream(_rtmp_ptr, 0) < 0)
			break;

		// Recving thread
		_running = true;
		_thread_ptr = new std::thread(thread_proc, this);
		if (NULL == _thread_ptr)
			break;

		success = true;
	} while (false);

	if (!success) {
		disconnect();
	}

	return success;
}

void CTestLibRTMPPuller::disconnect()
{
	_running = false;
	if (NULL != _thread_ptr) {
		_thread_ptr->join();
		delete _thread_ptr;
		_thread_ptr = NULL;
	}
	if (NULL != _rtmp_ptr) {
		RTMP_Close(_rtmp_ptr);
	}
}

void CTestLibRTMPPuller::thread_proc_internal()
{
	uint64_t frame_count = 0;

	RTMPPacket packet = { 0 };
	while (_running)
	{
		// FLV file
		int ret = RTMP_Read(_rtmp_ptr, (char *)_buffer_ptr, _buffer_size);
		if (ret < 0)
			break;
		if (ret == 0) // Timeout for recv
			continue;

		fwrite(_buffer_ptr, sizeof(uint8_t), ret, _file_ptr);

		frame_count++;
		if (frame_count % 100 == 0) {
			printf("read frames=%lld\n", frame_count);
		}
	}
}

bool CTestLibRTMPPuller::_init_sockets()
{
	WORD version;
	WSADATA wsaData;
	version = MAKEWORD(2, 2);
	return (0 == WSAStartup(version, &wsaData));
}

void CTestLibRTMPPuller::_cleanup_sockets()
{
	WSACleanup();
}
           
  • RTMP_Read

    直接将拉下来的流打包成FLV文件,可以使用播放器直接播放;
  • 但如果想获得音视频流做后续处理则不适合此种方式。

1.2 拉流解析出H264和AAC

对于需要H264和AAC流做后续处理的情况不适合使用

RTMP_Read

,可以使用

RTMP_ReadPacket

方式:

// Parse rtmp stream to h264 and aac
uint8_t nalu_header[4] = { 0x00, 0x00, 0x00, 0x01 };

int ret = RTMP_ReadPacket(_rtmp_ptr, &packet);
if (ret < 0)
	break;
if (0 == ret)
	continue;
if (RTMPPacket_IsReady(&packet)) {
	// Process packet, eg: set chunk size, set bw, ...
	RTMP_ClientPacket(_rtmp_ptr, &packet);

	if (packet.m_packetType == RTMP_PACKET_TYPE_VIDEO) {
		bool keyframe = 0x17 == packet.m_body[0] ? true : false;
		bool sequence = 0x00 == packet.m_body[1];

		printf("keyframe=%s, sequence=%s\n", keyframe ? "true" : "false", sequence ? "true" : "false");

		// SPS/PPS sequence
		if (sequence) {
			uint32_t offset = 10;
			uint32_t sps_num = packet.m_body[offset++] & 0x1f;
			for (int i = 0; i < sps_num; i++) {
				uint8_t ch0 = packet.m_body[offset];
				uint8_t ch1 = packet.m_body[offset + 1];
				uint32_t sps_len = ((ch0 << 8) | ch1);
				offset += 2;
				// Write sps data
				fwrite(nalu_header, sizeof(uint8_t), 4, _file_ptr);
				fwrite(packet.m_body + offset, sizeof(uint8_t), sps_len, _file_ptr);
				offset += sps_len;
			}
			uint32_t pps_num = packet.m_body[offset++] & 0x1f;
			for (int i = 0; i < pps_num; i++) {
				uint8_t ch0 = packet.m_body[offset];
				uint8_t ch1 = packet.m_body[offset + 1];
				uint32_t pps_len = ((ch0 << 8) | ch1);
				offset += 2;
				// Write pps data
				fwrite(nalu_header, sizeof(uint8_t), 4, _file_ptr);
				fwrite(packet.m_body + offset, sizeof(uint8_t), pps_len, _file_ptr);
				offset += pps_len;
			}
		}
		// Nalu frames
		else {
			uint32_t offset = 5;
			uint8_t ch0 = packet.m_body[offset];
			uint8_t ch1 = packet.m_body[offset + 1];
			uint8_t ch2 = packet.m_body[offset + 2];
			uint8_t ch3 = packet.m_body[offset + 3];
			uint32_t data_len = ((ch0 << 24) | (ch1 << 16) | (ch2 << 8) | ch3);
			offset += 4;
			// Write nalu data(already started with '0x00,0x00,0x00,0x01')
			//fwrite(nalu_header, sizeof(uint8_t), 4, _file_ptr);
			fwrite(packet.m_body + offset, sizeof(uint8_t), data_len, _file_ptr);
			offset += data_len;
		}
	}
	else if (packet.m_packetType == RTMP_PACKET_TYPE_AUDIO) {
		bool sequence = 0x00 == packet.m_body[1];

		printf("sequence=%s\n", sequence ? "true" : "false");

		// AAC sequence
		if (sequence) {
			format = (packet.m_body[0] & 0xf0) >> 4;
			samplerate = (packet.m_body[0] & 0x0c) >> 2;
			sampledepth = (packet.m_body[0] & 0x02) >> 1;
			type = packet.m_body[0] & 0x01;
			// sequence = packet.m_body[1];
			// AAC(AudioSpecificConfig)
			if (format == 10) {
				uint8_t ch0 = packet.m_body[2];
				uint8_t ch1 = packet.m_body[3];
				uint16_t config = ((ch0 << 8) | ch1);
				object_type = (config & 0xF800) >> 11;
				sample_frequency_index = (config & 0x0780) >> 7;
				channels = (config & 0x78) >> 3;
				frame_length_flag = (config & 0x04) >> 2;
				depend_on_core_coder = (config & 0x02) >> 1;
				extension_flag = config & 0x01;
			}
			// Speex(Fix data here, so no need to parse...)
			else if (format == 11) {
				// 16 KHz, mono, 16bit/sample
				type = 0;
				sampledepth = 1;
				samplerate = 4;
			}
		}
		// Audio frames
		else {
			// ADTS(7 bytes) + AAC data
			uint32_t data_len = packet.m_nBodySize - 2 + 7;
			uint8_t adts[7];
			adts[0] = 0xff;
			adts[1] = 0xf1;
			adts[2] = ((object_type - 1) << 6) | (sample_frequency_index << 2) | (channels >> 2);
			adts[3] = ((channels & 3) << 6) + (data_len >> 11);
			adts[4] = (data_len & 0x7FF) >> 3;
			adts[5] = ((data_len & 7) << 5) + 0x1F;
			adts[6] = 0xfc;
			// Write audio frames
			fwrite(adts, sizeof(uint8_t), 7, fp);
			fwrite(packet.m_body + 2, sizeof(uint8_t), packet.m_nBodySize - 2, fp);
		}
	}
	else if (packet.m_packetType == RTMP_PACKET_TYPE_INFO) {
		// TODO:
		// ...
	}
	else {
		// TODO:
		// ...
	}
}
           
  • RTMP_ReadPacket

    会自动组包,组包完成可以使用

    RTMPPacket_IsReady

    判断;
  • 对于音视频外的包要使用

    RTMP_ClientPacket

    做内部相应处理;
  • 对于视频包,包括视频配置包和视频数据包。配置包中可解析出SPS和PPS,数据包可解析出H264数据,解包过程其实是推流打包的逆过程。由于H264发送时候添加了

    0x00,0x00,0x00,0x01

    分隔符,因此无需额外添加,但解析出的SPS和PPS前需要添加;
  • 对于音频包,包括音频配置包和音频数据包。配置包包含音频配置信息,数据包包含AAC数据。RTMP推流时可以不发送音频配置包,服务器一般会自动生成;而推流的AAC也不必包含7字节的ADTS头,如果包含服务器也会自动去除。拉流下来的AAC数据其实也是不包含ADTS的,因此需要打上。

实际测试时遇到 RTMP_ReadPacket 崩溃的情况(不同URL情况不同),版本为librtmp2.4,VS2017。不知是编译出库的问题还是源码本身bug,欢迎留言交流。

此处只是提供思路,具体优化需要根据应用需求深入研究。

reference:

librtmp获取视频流和音频流1

继续阅读