LINUX.ORG.RU

Библиотека Sofia-SIP

 ,


3

1

Здравствуйте!

Пишу приложение с использованием библиотеки Sofia-SIP. Сессию организовать удалось. Есть вопрос:

Как можно организовать запись голоса в файл и чтение звука из файла в телефонную трубку? Буду очень признателен за информацию.

Как можно организовать запись голоса в файл

В тот момент, где ты достаёшь payload из rtp-пакета, записывай его в файл.

чтение звука из файла в телефонную трубку

Вместо записи звука с микрофона читай его из файла. Это, кстати, достаточно легко с помощью sipp сделать.

mono ★★★★★
()

тебе нечем заняться? возьми linphone там уже все реализовано и есть консольная версия.

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

В трубку чего? Задача как звучит? Тебе нужно послать какой-то rtp-поток на «железный» сип-телефон?

mono ★★★★★
()

Если с помощью Sofia-SIP то никак, она не занимается RTP только SIP. Для RTP потока бери другую либу или напиши свою (если использовать кодеки с константным размером фрейма то там сендер делается за пару часов (за час если знать RFC), с приемом бедет посложнее из-за житер буфера - но на него можно забить если все будет в локальной сетке)

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

Задача примерно такая:

Есть железный сип-телефон, есть «обычный» телефон. Обычный телефон записывает голос абонента в файл (там свои заморочки, но все реализовано), этот файл читается в трубку сип-телефона и наоборот.

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

Что именно по подробней ?

Если не знаком с общей инфраструктурой VoIP/SIP то в общих чертах она состоит из двоих независимых частей: сигналинг (протокола инициализации сессий - SIP) и непостредственно меди потоков (RTP). По большому сщету сигналинг понятия не имеет как там передаются медии (это не совсем правда но можно сщитать что так и есть), его задача передать какуюто мета информацию от одного абонента другому (и проинформировать о изменении состояния сессии). Этой мето информацией является SDP - по сути строка в которой описывается каким образом абонент сможет получать медию (IP, порт, кодеки) - но сигналингу (SIP-у) обсалютно все равно что там в этом SDP находится, для него это строка (это немного похоже на FTP протокол - там тоже несколько соединений используется 1 главное для сигналинга, и много второстепенных для передачи данных). Так вот для полноценного VoIP/SIP клиента нужно реализовывать 2 технологии - 1: SIP протокол, и RTP: потоки.

Sofia-SIP - это сугубо библиотека для SIP, RTP там нету.

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

Недавно решал подобную задачу (тоже нужно было воспроизводить записаную медию по сип звонку), я посмотрел на библиотеки для RTP - ничего толкового не нашол (искал правда не сильно долго), в результате все собрал на бустовских таймерах и УДП сокетах (потратил гдето дня два).

Перед передачей 200 ОК телефону открывай 2 UDP сокета (один обязательно на четном порту N второй номер порта должен быть N+1) на эти порты тебе телефон будет слать медию (можеш ее игнорировать или записывать в файл) и отправляй SDP вида

m=audio <N> RTP/AVP <codec id>
a=rtpmap:<codec id> <codec info>
В том SDP который тебе приходит от телефона будет подобная запись из которой можно понять куда ему слать медию (если он не за натом конечно), заводи таймер и по таймеру формируй RTP пакеты и отправляй на телефон - вот и все.

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

Основная задача конечно будет с кодеками - если тебе нужно будет потдерживать большой перечень кодеков будет много мороки, если нет то тогда бери какойнибудь монотонный кодек который потдерживает телефон (например G722 или вообще PCMU если сетки не жалко), тогда у тебя каждый RTP пакет будет одинаковой длинны и все можно сваливать в файл без никаких разделителей + потом эти файлы можно будет легко конвертировать из/в mp3/wav с помощью ffmpeg.

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

gstreamer - для RTP нет не смотрели даже в этом направлении (хотя для декодинга используем в некоторых моментах). Сама организация исходящего / входящего RTP потока задача легкая (2-10 килобайт на С++) такчто особо не замарачивались с либами, а то получается что интеграция выходит дороже чем реализация с нуля.

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

А что именно интерисует ? Отправка/получение RTP пакетов? Или как связать RTP c сипом/SDP?

Просто если хотите разобраться то лутше я вам постараюсь набросать общую концепцию что за чем происходит/делается + куски кода. А если хотите просто попробывать интегрировать код - то лутше поискать либу :)

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

Я делаю так - когда получаю INVITE с трубки, отвечаю 200ОК+SDP. Перед этим, как Вы и сказали, делаю два UDP-сокета (5004 для приема пакетов и еще один для передачи). Соответственно, в 5004 порт кидаются RTP-пакеты с трубки. Я пока не знаю, как мне организовать запись пакета в файл (fwrite-ом можно записать, но как мне выделить тело пакета...). И правильно я понимаю, после выделения тела, нужно его сначала через кодек пропустить, а уже только потом записывать?

И да, интересует как раз прием/отправка RTP. C SIP я вроде разобрался. Там нужно было сделать простейшую сигналку (без переадресации, удержания и т.д.).

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

5004 для приема пакетов и еще один для передачи

Нет в SDP вам приходит номер порта на который ВЫ можете слать свои RTP данные (например 8002), а на 8003 порт ВЫ можете слать RTCP данные (это пакеты которые организовывают контроль потока, для вашего случая они не нужны, в принципе они используются только в видео потоках -но порт вам открывать все равно нужно, если вдруг телефон решит послать вам какуюто статистику по потоку но получит ICMP unrecheable возможно он прибъет поток). Соотвецтвенно вы отправлете порт 5004 на который вам телефон будет присылать свой RTP поток а на 5005 телефон будет присылать RTCP данные (а может и не будет, в любом случае вы его открываете но все что на него приходит игнорируете).

Далее вы получили на свой 5004 порт UDP пакет в какойто буфер

char rtpPacket[2048];
recv(udpSock5004, rtpPacket, sizeof(rtpPacket));
Теперь вы просто отбрасываете заголовок из своего RTP пакета и получаете данные которые можно либо сохранять в файл и потом их можно будет читать и отправлять в RTP поток без никакого енкодинга/декодинга либо декодировать в PCM (если нужно).
#pragma pack(push, 1)
struct RtpHdr
{
#ifdef SYSTEM_BIG_ENDIAN
    unsigned char version:2;   /* protocol version */
    unsigned char p:1;         /* padding flag */
    unsigned char x:1;         /* header extension flag */
    unsigned char cc:4;        /* CSRC count */
    unsigned char m:1;         /* marker bit */
    unsigned char pt:7;        /* payload type */
#else
    unsigned char cc:4;        /* CSRC count */
    unsigned char x:1;         /* header extension flag */
    unsigned char p:1;         /* padding flag */
    unsigned char version:2;   /* protocol version */
    unsigned char pt:7;        /* payload type */
    unsigned char m:1;         /* marker bit */
#endif
    uint16 seq;             /* sequence number */

    uint32 ts;               /* timestamp */
    uint32 ssrc;             /* synchronization source */
};
#pragma pack(pop)

char *audioData = rtpPacket + sizeof(RtpHdr);
write(outFile, audioData, rtpPacketSize - sizeof(sizeof(RtpHdr)));
RtpHdr *hdr = (RtpHdr)(rtpPacket);
printf
(
"RTP Header [v:%i, pt: %i, cc: %i, seq: %i, ts: %u, ssrc: %u]",
(int)(hdr->version),
(int)(hdr->pt),
(int)(hdr->cc),
(unsigned)( ntohs(hdr->seq) ),
(unsigned)( ntohl(hdr->ts) ),
(unsigned)( ntohl(hdr->ssrc) )
);
Стоит обратить внимание что все поля в RTP заголовке передаются в сетевой нотации и для их корректной обработки нужно переводить в нотацию хоста (ntohs). Данный пример не будет работать если в заголовке присутствуют дополнительные CSRC (cc > 0) - но как правило cc всегда 0. Плюс есть вероятность что некоторый пакет будет потерян или пакеты придут не в том порядке в котором телефон их отпровлял (из за не идеального канала связи) - пока можете это игнорировать, но для решения таких проблем используются поля заголовка: seq, ts, ssrc.

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

Да и вы можете отправлять свой поток с любого порта (и в теории с любого IP), в SDP указываются только порты НА которые будет идти прием потока, про отправку ничего не говорится. Но существует общепринятое правило что отправка идет с тех-же портов на которые и осуществляется прием данных (симетричный RTP) - в этом случае можно легко решать проблему когда один из абонентов находится за натом.

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

C SIP я вроде разобрался.

Помимо сипа почитайте нужно разобратся есще с SDP, например вам нужно научится читать чтото типа такого:

v=0
o=202 2730 2730 IN IP4 1.2.3.4
s=Talk
c=IN IP4 1.2.3.4
t=0 0
m=audio 7078 RTP/AVP 112 111 110 0 8 104 101
a=rtpmap:112 speex/32000
a=fmtp:112 vbr=on
a=rtpmap:111 speex/16000
a=fmtp:111 vbr=on
a=rtpmap:110 speex/8000
a=fmtp:110 vbr=on
a=rtpmap:104 AMR/8000
a=fmtp:104 octet-align=1
a=rtpmap:101 telephone-event/8000
a=fmtp:101 0-11

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

Так, прием RTP и запись в файл я сделал, спасибо. Теперь вопрос, а как читать из файла в трубку? Нужно читать из файла и отправлять сообщения с RTP-заголовком?

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

Очевидно что да :) Основные поля в RT заголовке:

version - версия протокола (всегда 2)
p - выравнивание (ставь в 0)
x - расширение (ставь в 0)
cc - ко-во CSRC (ставь в 0)
m - маркер (ставь в 0, но могут быть вариванты для не-рых кодеков)
pt - сдесь ID кодека (номер такойже как был в SDP)
seq - номер пакета (при каждой отправке / приему он увеличивается на 1)
ts - таймстамп данных (почти тоже самое что и seq но от пакета к пакету прибовляется не 1 а к-во аудио семплов в пакете (как правило это константа).
ssrc - ID потока (рандомное число), если поток не меняется то для всех пакетов он одинаков.
Тебе нужно завести таймер и каждые 20 милисекунд (для некоторых кодеков время может быть другое) отравлять пакет на телефон.

Для начала я бы посоветовал обойтись без файла - сделай ехо, тоесть принимаеш RTP пакет отрезаеш от него пайлоад (аудио данные) и из него формируеш новый пакет со своим RTP заголовком - и отправляеш его назад на телефон. В результате если позвонить и говорить в телефон должен ты должен услышать себя (тут тебе хоть не нужно будет возиться с таймерами и проще отлаживатся). Если эхо заработает нормально - прикручивай файлы.

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

Спасибо, Вас тоже!

С какой целью кодеки нужны ? (перекодировать сигнал на лету, или просто конвертировать скажем из/в WAV/MP3).

Если нужно все делать на лету то берите библиотеку с нужным декодером / енкодером (нужно искать), настраиваете кодек в соотвецтвии с параматрами из SDP (частота, к-во каналов) далее просто передаете пайлоад из каждого RTP пакета в кодек получаетте на выходе PCM (который в теории можно отдовать в на вход звуковой карте) - после может понадобится сделать ресамплинг полученого PCM (сменить ему частоту дискритизации и/или разбить на фреймы другой длительности) - когда PCM соотвецтвует требованиям энкодера отдаете его на вход энкодера на выходе получаете сжатые данные к ним добовляете RTP заголовок и отправляете абоненту.

Если нужно конвертировать оффлайн - то проще взять каую нибудь командлайн утелиту которая сможет переконвертировать сохраненный пайлоад. Например

ffmpeg -i dump.g722 -acodec libmp3lame -ab 64 -f mp3 out.mp3
сконвертирует сохраненный пайлоад (g722 кодек) в mp3 файл

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

rtpPacketSize у Вас чему равен?

Это размер принятого UDP пакета.

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

Ну и вдогонку вопрос - как можно распарсить SDP сообщение?

Чем угодно, чем обычно строки парсят. В принципе в софие есть встроенный SDP парсер можно им, я правда API его не помню уже - но он там точно есть (в SDP модуле).

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

Большое спасибо за помощь!!

Прием/передача RTP заработали.

Но есть одно НО - после записи медиа в файл, читаю его через fread-sendto в трубку - слышно голос, но с помехами/искажениями. Подскажите пожалуйста, с чем это может быть связано? С кодеками? Или нужно читать из файла данные того же размера, как они и были записаны? (Или это не играет роли?)

Примечание: телефон использую один и тот же.

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

Незачто.

Если есть проблемы с кодеком то скорее всего будет либо шум, либо тишина. Если реч слышно но она с помехами то скорее всего проблема на транспортном уровне (либо гдето что-то теряется(байтики, или пакеты целиком), либо пакеты местами переставляются, либо сами пакеты портятся частично).

Да размер данных должен быть именно такой какой приходил от телефона (каждый пакет это определенный интервал аудио данных, как правило 20 милисекунд. И если вы например пошлете в одном пакете аудио даннх на 15 или 25 милисекунд то кодек их будет «подгонять» под 20 - и получатся искожения + есть вероятность что кодек вообще не сможет их декодировать).

Если Вы помните я советовал использовать «монотонный» кодек (например G722, PCMU, PCMA и тд.) - в таких кодеках одинаковый интервал времени всегда кодируется в одинаковое ко-во байт. Следовательно ВСЕ RTP пакеты будут абсолютно одинаковой длинны и Вам не нужно будет мучаться с длинной пайлоад данных - она будет константной.

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

Возможно проблема в таймпстампах (ts), секвенс намберу(seq), неверно разбиваете файл на пакеты или плохо тикает ваш таймер (не четко отрабатывает 20 милисекундный интервал).

Скорее всего проблема с таймером - если слать четко по таймеру то очень высокая вероятность что таймер будет тикать плохо и звук будет с треском, лутше по таймеру отдельно проверять сколько прошло времени и перещитовать сколько нужно отпавить пакетов ...

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

Посмотрите пожалуйста, правильно ли я записываю и формирую RTP-пакеты?

Запись:

	output = fopen("/tmp/output.PCMA", "wb");
	for (i = 0; i < 100000; i++) {
		char rtpPacket[1024];
		len = recvfrom(s1, rtpPacket, 1024, 0, (struct sockaddr *) &si_other, &slen);
		char *audioData = rtpPacket + sizeof(RtpHdr);
		hdr = (struct RtpHdr*)(rtpPacket);
		fwrite(audioData, strlen(audioData), 1, output);
		printf
		(
		"%d, RTP Header [v:%i, pt: %i, cc: %i, seq: %i, ts: %u, ssrc: %u]\n", strlen(audioData),
		(int)(hdr->version),
		(int)(hdr->pt),
		(int)(hdr->cc),
		(unsigned)( ntohs(hdr->seq) ),
		(unsigned)( ntohl(hdr->ts) ),
		(unsigned)( ntohl(hdr->ssrc) )
		);
		usleep(20000);
	}
	fclose(output);

Чтение и формирование:

output = fopen("/tmp/output.PCMA", "rb");
			int ts = 0, seq = 0, ssrc = 1000000000 + rand()%8999999999;
			srand(time(NULL));
			while (!feof(output)) {
				char rtpPacket[173] = {0}, audioData[161] = {0};
				fread(audioData, 160, 1, output);
				hdr_out = malloc(sizeof(struct RtpHdr));
				hdr_out->version = 2;
				hdr_out->p = 0;
				hdr_out->x = 0;
				hdr_out->cc = 0;
				hdr_out->m = 0;
				hdr_out->pt = 8;
				ts += 160;
				hdr_out->seq = htons(++seq);
				hdr_out->ts = htonl(ts);
				hdr_out->ssrc = htonl(ssrc);		
				sprintf(rtpPacket, "%s%s", (char *)(hdr_out), audioData);
				sendto(s1, rtpPacket, 172, 0, (struct sockaddr *) &si_other, slen);
				usleep(20000);
			}

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

Вот эта строка скорее всего неправильная:

sprintf(rtpPacket, "%s%s", (char *)(hdr_out), audioData);

Размер пакета на 10 меньше получается, чем изначально был.

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

иди уже подучи язык что ли

Спасибо, уже над этим работаю.

jockerface
() автор топика
Ответ на: комментарий от jockerface
fwrite(audioData, strlen(audioData), 1, output);

strlen здесь вообще ни к чему, здесь строк нету реальный размер аудио данный в пакете это (len - sizeof(RtpHdr)); тоесть код должен быть

fwrite(audioData, (len - sizeof(RtpHdr)), 1, output);
Также при записи usleep явно лишний, recvfrom будет ожидать пакета если его нету (при блокируещем IO), а с usleep можно получить переполнение буфера и потерю данных. + не хватает обработки ошибок (тогоже recfrom и fwrite), но вобщем с таким кодом можно получить аудио данные хотя они скорее всего будут немного испорчены (из за strlen в fwrite), иногда этот код может приводить к сегфолту по тойже причине (вам явно не хватает практики работы с С).

Чтение и отправка выглядит ужасно :), он не понятно что делает но явно не занимается отправкой аудио в RTP. Должно быть чтото вроде этого (код не проверяю поэтому может и не скомпилируется):

// Новая структура для RTP с PCMA/PCMU данными
#pragma pack(push, 1)
struct RTP_PCMA
{
   struct RtpHdr hdr;
   char audioData[160];
};
#pragma pack(pop)
// .....

struct RTP_PCMA outPacket;
int ts = 0, seq = 0, ssrc = 1000000000;
srand(time(NULL)); // srand нужно вызывать ДО rand иначе от него толку 0
ssrc += rand()%8999999999;

// Заполняем поля заголовка которые не меняются для всего потока
outPacket.hdr.version = 2;
outPacket.hdr.p       = 0;
outPacket.hdr.x       = 0;
outPacket.hdr.cc      = 0;
outPacket.hdr.m       = 0;
outPacket.hdr.pt      = 8;
outPacket.hdr.ssrc = htonl(ssrc);

while (!feof(output)) {
   fread(outPacket.audioData, 160, 1, output);
   ts += 160;
   outPacket.hdr.seq = htons(++seq);
   outPacket.hdr.ts  = htonl(ts);
		
   sendto(s1, &outPacket, sizeof(outPacket), 0, (struct sockaddr *) &si_other, slen);
   usleep(19500);
}
Вот както так, но сдесь есть есще нюанс с usleep, как вы могли заметить я там поставил 19500 вместо 20000 - связано с тем что помимо самого usleep есть есще накладные расходы на чтение и отправку и их нужно учитывать (лутше отправлять пакеты чаще чем реже). Но 19500 тоже не решение проблемы - сдесь нужно делать собственный контроль по времени (по средствам clock_gettime(CLOCK_MONOTONIC, &ts) ). Тоесть usleep используете исключительно для того чтоб «убить» время, и скажем засыпаете на 10 милисекунд (вместо 20). Но на каждой итерации цыкла через clock_gettime(CLOCK_MONOTONIC, &ts) проверяете сколько прошло времени после последней отправки RTP пакета, если меньше 20 милисекунд - то ничего не делаете если же больше то отправляете такое ко-во пакетов сколько нужно (например если прошло 40 милисекунд то отправляете сразу 2 пакета). Но для начала попробуйте с usleep(19500) - должно быть более мение приемлимо, потом уже можно будет усовершенствовать ...

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

Спасибо! С этими вещами разобрался. Появился новый вопрос: подскажите пожалуйста, как сделать транскодирование с 711 кодека в 722? Вроде бы понятно, что нужно сначала разжать информацию, а потом её снова сжать, только другим методом. Но как это сделать...

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

А как думаете, насколько быстро будет проходить передача данных, если поймать UDP-пакет из трубки, записать медию в файл и преобразовать ffmpeg'ом?

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

Примечание: и сделать так с каждым приходящим пакетом.

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