LINUX.ORG.RU

Помогите реализовать RTP протокол.

 , ,


2

3

Всем привет! Может подскажете как решить такую досадную проблему? Пишу небольшой RTP сервер для стриминга H.264 видео.

Предстала неизвестная проблема: через некторое время(20-30 секунд) перестает корректно воспроизводится стрим(картинка рассыпается). Я подозреваю, что как-то не так упаковываю кадры H.264 в RTP.

Кадры формируются аппаратным кодировщиком Raspberry Pi, работа с которым осуществляется через OpenMAX. Источник данных для кодировщика - камера. Ну, это собственно не так важно.

Полученыей кадр передаются небольшой нижележащей функции(send_data_to_rtp), которая пакует его в RTP. Принимает аргументы: data - указатель на изображение в памяти, len - размер изображения, framerate - колличество кадров в секунду.

// заголовки
	#define BUF_SIZE		1500
	#define RTP_PAYLOAD_MAX_SIZE	1400

	/* RTP ЗАГОЛОВОК. 12 байт */
	typedef struct{
		/* первый байт */
		uint8_t csrc_len: 	4;	
		uint8_t extension:	1;	
		uint8_t padding:	1;	
		uint8_t version:	2;	
		/* второй байт */
		uint8_t payload_type:  	7;	
		uint8_t marker:		1;	
		/* третий-четвертый байты */
		uint16_t seq_no;		
		/* пятый-восьмой байты */
		uint32_t timestamp;		
		/* девятый-двенадцатый байт */
		uint32_t ssrc;			
	}__attribute__ ((packed)) rtp_header;

	typedef struct {
		uint8_t type:		5;	
		uint8_t nri: 		2;	/*  */
		uint8_t f:		1;	/*  */
	}__attribute__ ((packed)) nalu_header;


	typedef struct {
		uint8_t type: 5;
		uint8_t nri: 2;
		uint8_t f: 1;
	} __attribute__ ((packed)) fu_indicator;


	typedef struct {
		uint8_t type: 5;
		uint8_t r: 1;
		uint8_t e: 1;
		uint8_t s: 1;
	} __attribute__ ((packed)) fu_header;

.......
// Формируем пакет
static void send_data_to_rtp(uint8_t *data, int len, int framerate)
{
	static uint8_t sendbuf[BUF_SIZE];
	static uint32_t ts_current = 0;
	static uint16_t seq_num = 0;
	static uint16_t pack_num, last_pack_size, current_pack;

	uint8_t *nalu_playload;
	/* заголовок RTP */
	rtp_header *rtp_hdr;
	/* Заголовок NALU */
	nalu_header *nalu_hdr;
	/* Заголовк NALU фрагментированых данных */
	fu_indicator *fu_ind;
	/* заголовк NALU фрагментированых данных. Идентифицирует фрагмент */
	fu_header *fu_hdr;

	ts_current += (90000 / framerate);

	memset(sendbuf, 0, sizeof(sendbuf));

	rtp_hdr = (rtp_header*)&sendbuf[0];
	
	rtp_hdr->version = 2;
	rtp_hdr->marker = 0;
	rtp_hdr->csrc_len = 0;
	rtp_hdr->extension = 0;
	rtp_hdr->padding = 0;
	rtp_hdr->ssrc = htonl(SSRC_NUM);
	rtp_hdr->payload_type = TYPE_H264;
	rtp_hdr->timestamp = htonl(ts_current);

	if (len <= RTP_PAYLOAD_MAX_SIZE) {
		rtp_hdr->marker = 1;
		rtp_hdr->seq_no = htons(++seq_num);

		nalu_hdr = (nalu_header*)&sendbuf[12];
		/* тип фрагмента */
		nalu_hdr->type = data[0] & 0x1f;
		nalu_hdr->f = data[0] & 0x80;
		nalu_hdr->nri = data[0] & 0x60 >> 5;

		nalu_playload = (uint8_t*)&sendbuf[13];
		/* Копирую буфер без ЗАГОЛОВКА(т.е. первого байта, являющийся заголовком NALU)! */
		memcpy(nalu_playload, data + 1, len-1);
		
		send_data_client(sendbuf, len + 13);
	} else {
		/* пакет не помещается в MTU, значит будем фрагментировать. 
		* Начало s = 1, e = r = 0
		* Середина s = 0, e = 0  r = 0
		* Конец s = 0, e = 1 r = 0
		*/
		/* определяю требуемое колличество пакетов */
		pack_num = (len % RTP_PAYLOAD_MAX_SIZE) ? (len / RTP_PAYLOAD_MAX_SIZE + 1) : (len / RTP_PAYLOAD_MAX_SIZE);
		/* размер данных в последнем пакете */
		last_pack_size = (len % RTP_PAYLOAD_MAX_SIZE) ? (len % RTP_PAYLOAD_MAX_SIZE) : (RTP_PAYLOAD_MAX_SIZE);

		current_pack = 0;

		/* описываю заголовки, которые используются на каждой иттерации */
		fu_ind = (fu_indicator *)&sendbuf[12];
		fu_ind->f = data[0] & 0x80;
		fu_ind->nri = (data[0] & 0x60) >> 5;
		/* говорим, что пакет у нас фрагментирован */
		fu_ind->type = 28;
		fu_hdr = (fu_header *)&sendbuf[13];
		fu_hdr->type = data[0] & 0x1f;
		
		while (current_pack < pack_num) {
			rtp_hdr->seq_no = htons(++seq_num);
			/* первый пакет */
			if(current_pack == 0) {
				/* начало фрагментированого блока */
				fu_hdr->s = 1, fu_hdr->e = 0, fu_hdr->r = 0;
				rtp_hdr->marker = 0;
				
				nalu_playload = (uint8_t*)&sendbuf[14];
				memset(nalu_playload, 0, RTP_PAYLOAD_MAX_SIZE);
				/* Копирую буфер без ЗАГОЛОВКА(т.е. первого байта, являющийся заголовком NALU)! */
				memcpy(nalu_playload, data + 1, RTP_PAYLOAD_MAX_SIZE);
				send_data_client(sendbuf, RTP_PAYLOAD_MAX_SIZE + 14);
			/* середина */
			} else if(current_pack < pack_num - 1){
				fu_hdr->s = 0, fu_hdr->e = 0, fu_hdr->r = 0;
				rtp_hdr->marker = 0;

				nalu_playload = (uint8_t*)&sendbuf[14];
				memset(nalu_playload, 0, RTP_PAYLOAD_MAX_SIZE);

				memcpy(nalu_playload, data + (current_pack * RTP_PAYLOAD_MAX_SIZE) + 1, RTP_PAYLOAD_MAX_SIZE);
				send_data_client(sendbuf, RTP_PAYLOAD_MAX_SIZE + 14);
			/* последний пакет */
			} else {
				rtp_hdr->marker = 1;
				nalu_playload = (uint8_t*)&sendbuf[14];
				fu_hdr->s = 0, fu_hdr->e = 1, fu_hdr->r = 0;
				memset(nalu_playload, 0, RTP_PAYLOAD_MAX_SIZE);

				memcpy(nalu_playload, data + (current_pack * RTP_PAYLOAD_MAX_SIZE) + 1, last_pack_size - 1);
				send_data_client(sendbuf, last_pack_size - 1 + 14);
			}
			current_pack += 1;
		}
	}
}




// Тут пишу в сокет.
static void send_data_client(uint8_t *send_buf, size_t len_sendbuf)
{
	sendto(socket_fd, send_buf, len_sendbuf, 0, (struct sockaddr *)&addr,sizeof(addr));
}

Один из фреймов выгялит след.образом: 0000 0001 2588 804f ec82 b9bf e0b7 1789 .....

До вызова функции send_data_to_rtp отсекаю 00 00 00 01. После заголовка RTP(12 байт) и еще двух байтов(заголовки для фрагментации) последовательность в готовом пакете выгядит так 88 804f ec82 b9bf e0b7 1789 .....

Данные фрейма в правильном порядке «растекаются» по пакетам(смотрел в вайршарке) ничего лишнего не копируется и не теряется. Следовательно вопрос: правильно ли я формирую заголовки пакетов?

При воспроизведении появляются ошибки, вида(проигрывал стрим в mplayer):

V:  22.3   0/  0 10%  1%  0.0% 0 0 
[h264 @ 0xb6497640]error while decoding MB 1 14, bytestream (-5)
[h264 @ 0xb6497640]concealing 68 DC, 68 AC, 68 MV errors
V:  22.3   0/  0 10%  1%  0.0% 0 0 
[h264 @ 0xb6497640]error while decoding MB 16 14, bytestream (-9)
[h264 @ 0xb6497640]concealing 53 DC, 53 AC, 53 MV errors
V:  22.6   0/  0 10%  1%  0.0% 0 0 
[h264 @ 0xb6497640]error while decoding MB 17 3, bytestream (-3)
[h264 @ 0xb6497640]concealing 272 DC, 272 AC, 272 MV errors
V:  22.7   0/  0 10%  1%  0.0% 0 0 

Буду премного благодарен за любой совет! Спасибо!



Последнее исправление: batchar (всего исправлений: 14)

Я, RTP, протокол, что случилось?
Шутка. ;-)

Deleted
()
Ответ на: комментарий от anonymous

wireshark

Он мне рассказал, следующие: номер последовательности соблюдается, время выставляется корректно, ему было известно, что передаю H.264. Все байты фрейма «растеклись» по пакетам. Нет потерь и нет лишних. В вайршакре есть инструменты для поиска ошибок в RTP видеопотоке?

batchar
() автор топика

Если картинка есть (первые 20-30 секунд) - то скорее всего с заголовками все нормально. А «рассыпается» она скорее всего из за того что гдето теряется пакет в результате портится какойто блок, далее приходит P-frame который ссылается на потеряный блок и тоже «выподает» - со временем ошибки накапливаются и картинка полсностью рассыпается. По идее ситацию должен исправить следующий I-Frame (ключевой кадр) но неизвестно как часто они у вас генерируются источником.

Возмоно конечно что проблемы таки в протоколе (в часности с секвунс намбером или таймстемпам) - но покоду вроде нормально с ними все.

zaz ★★★★
()
Ответ на: wireshark от batchar
pack_num = (len % RTP_PAYLOAD_MAX_SIZE) ? (len / RTP_PAYLOAD_MAX_SIZE + 1) : (len / RTP_PAYLOAD_MAX_SIZE);
		/* размер данных в последнем пакете */
last_pack_size = (len % RTP_PAYLOAD_MAX_SIZE) ? (len % RTP_PAYLOAD_MAX_SIZE) : (RTP_PAYLOAD_MAX_SIZE);

Помоему у вас сдесь проблемка не большая, вы высщитывате ко-во пакетов и размер последнего пакета на основе размера пайлоада (len) а фактически фрагментируете (len-1) байт (NALU заголовок не фрагментируется). В резултате если у вас на входе был len=(RTP_PAYLOAD_MAX_SIZE+1) то вы получите last_pack_size=1. А далее

memcpy(nalu_playload, data + (current_pack * RTP_PAYLOAD_MAX_SIZE) + 1, last_pack_size - 1);
send_data_client(sendbuf, last_pack_size - 1 + 14);
В последнем пакете будет только NALU заголовок без данных, может это и не страшно и проблема не в этом - но както выглядет не очень корректно.

zaz ★★★★
()
Последнее исправление: zaz (всего исправлений: 1)
Ответ на: комментарий от zaz

Спасибо. Что-то я упустил этот момент.

batchar
() автор топика
Ответ на: комментарий от zaz

Я увеличил число ключевых кадров. Теперь происходит следующие: данные отображаются с большой задержкой(> секунды, до прихода след. ключевого кадра, я так полагаю). I-frame является через каждые 30 кадров.
Картинка рассыпается в ffplay и mplayer(в vlc не рассыпается). Смотря на ffplay у меня сложилось впечатление, что все P-кадры неправильно декодируются. Меньший интервал появления I-frame проблемы не решил.

Перед каждым I-frame генерирую [SPS] [PPS] кадры в виде отдельных пакетов. Скажите пожалуйста, это правильно?

Поток выгядит так:
[SPS]
[PPS]
[RTP I-Frame]
[RTP P-FRAME]
[RTP P-FRAME]
[RTP P-FRAME]
....
[SPS]
[PPS]
[RTP I-Frame]

Лог vlc:

[h264 @ 0x90bba80] error while decoding MB 19 14, bytestream (-3)
[h264 @ 0x90bba80] concealing 50 DC, 50 AC, 50 MV errors
[h264 @ 0x90bb680] error while decoding MB 19 14, bytestream (-3)
[h264 @ 0x90bb680] concealing 50 DC, 50 AC, 50 MV errors
[h264 @ 0x90bb680] error while decoding MB 19 14, bytestream (-3)
[h264 @ 0x90bb680] concealing 50 DC, 50 AC, 50 MV errors

Входные данные в функцию генерируются энкодером OpenMAX. Он скидывает мне данные в буфер, этот буфер направляю в функцию send_data_to_rtp. Перед каждым I-frame энкодер наполяет буфер PPS, а потом SPS данными. Их оборачиваю как обычные пакеты.

Скажите пожалуйста, правильно ли я делаю?

batchar
() автор топика
Ответ на: комментарий от batchar

Я не занимался передачей H.264 потока по RTP но вообще это не так просто, там есть много нюансов и процес инкапсуляции видео данных в RTP поток на порядок сложнее чем для аудио. RTP сервер должен уметь обробатывать фитбеки (NAC, FEC) от приемника, понижать битрейты - ретрансмитить потеряные пакеты и тд. Плюс есть несколько режимов и стандартов на данные операции - приемнику нужно их както расказывать в каком режиме бедет передоватся поток. Советую почитать RFC на эту тему (например RFC 3984 RTP Payload Format for H.264 Video)

zaz ★★★★
()
Последнее исправление: zaz (всего исправлений: 1)
Ответ на: комментарий от batchar

в гугле столько уже готовых примеров, тот же mediastream by linphone, но вы в упор пытаетесь угадывать сами

anonymous
()
Ответ на: комментарий от zaz

Да, я еще раз просмотрю RFC. Фишка в том, что я не могу использовать RTCP протокол, т.к. данные c устройства будут передаваться по радиоканалу в одну сторону(будет использоваться на БПЛА).

batchar
() автор топика
Ответ на: комментарий от batchar

Тогда боюсь вам не подойдет RTP/H.264 поскольку радио канал подразумевает большую потерю данных, а если у вас не будет реализован механизм перезапроса потеряных фрагментов - картинка будет с большими искажениями вплоть до полной непригодности. Нужен либо кодек который устойчев к потере данных, либо протокол передачи данных с избыточнастью для автоматической коррекции ошибок (но второй варинт я бы не советовал - получите оверхед по траффику но все равно при сильно не устойчевом канале картинка рассыпится). Я бы на вашем месте посмотрел как организовывают ретрансляцию спутникового/кабельного цифравого TV (ATSC) - там ситуация схожая, есть односторонний видео поток и не надежный радио канал.

zaz ★★★★
()
Ответ на: комментарий от anonymous

Не все так просто)

Отдальные порции данных должны быть отделены последовательностью 00 00 01. При упакевке в RTP это все отсекается.

Достаточно просто решается несколькими строками питоновского кода. Но, как я говорил последователность байтов не теряется.

batchar
() автор топика
Ответ на: комментарий от zaz

в iptv нормально работает передача h264 в одном направлении, без всякой обратки

anonymous
()
Ответ на: комментарий от zaz

Спасибо огромное вам за ответы и проявленый интерес к теме! Ларчик просто открывается, оказывается. FPS почему-то отличается, от установленного мною значения. Я просто выставил параметры, запрошенные у энкодера после установке моих значений, работает! :)

batchar
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.