Материал статьи взят с моего дзен-канала.
Передача звукового сигнала через RTP-поток
В прошлой статье мы собрали схему дистанционного управления из генератора и детектора тональных сигналов, которые работают внутри одной программы. В этой статье мы научимся использовать протокол RTP (RFC 3550 — RTP: A Transport Protocol for Real-Time Applications) для приема/передачи звукового сигнала по Ethernet-сети.
Протокол RTP (Real Time Protocol) в переводе означает протокол реального времени, он используется для передачи звука, видео, данных, всего того, что требует передачи в режиме реального времени. В качестве примера возьмем звуковой сигнал. Гибкость протокола такова, что позволяет передавать звуковой сигнал с наперед заданным качеством.
Передача выполняется с помощью UDP-пакетов, что означает что при передаче вполне допускается потеря пакетов. В каждый пакет вкладывается специальный RTP-заголовок и блок данных передаваемого сигнала. В заголовке содержится случайно выбираемый идентификатор источника сигнала, информация о типе передаваемого сигнала, уникальный порядковый номер пакета, для того чтобы пакеты при декодировании могли быть выстроены в правильном порядке, независимо от того в каком порядке их доставила сеть. Заголовок также может содержать дополнительную информацию, так называемое расширение, которое позволяет адаптировать заголовок к применению в конкретной прикладной задаче.
Блок данных содержит полезную нагрузку пакета. Внутренняя организация содержимого зависит от типа нагрузки, это могут быть отсчеты монофонического сигнала, стереосигнал, строка видео изображения и т.д.
Тип нагрузки обозначается семибитным числом. Рекомендация RFC3551 (RTP Profile for Audio and Video Conferenceswith Minimal Control) устанавливает несколько типов нагрузки в соответствующей таблице приведены описание типов нагрузки и значение кодов которыми они обозначаются. Часть кодов не имеют жёсткой привязки к какому-либо типу нагрузки-они могут использоваться для обозначения произвольной нагрузки.
Размер блока данных ограничен сверху максимальным размером пакета, который может быть передан в данной сети без сегментирования (параметр MTU). В общем случае это не более 1500 байт. Таким образом, чтобы увеличить количество передаваемых в секунду данных можно до определенного момента увеличивать размер пакета, а затем уже потребуется увеличивать частоту отправки пакетов. В медиастримера это настраиваемый параметр. По умолчанию он равен 50 Гц, т.е. 50 пакетов в секунду. Последовательность передаваемых RTP-пакетов будем называть RTP-потоком.
Чтобы начать передачу данных между источником и приемником, достаточно, чтобы передатчик знал IP-адрес приёмника и номер порта, который тот использует для приема. Т.е. без всяких предварительный процедур источник начинает передавать данные, а приёмник в свою очередь готов немедленно их принять и обработать. По стандарту, номер порта используемый для передачи или приема RTP-потока должен быть четным.
В ситуациях, когда нельзя наперед знать адрес приёмника, используются сервера, на которых приемники оставляют свой адрес, а передатчик может его запросить, сославшись на некое уникальное имя приемника.
В случаях, когда качество канала связи или возможности приёмника неизвестны организуется канал обратной связи, по которому приёмник может информировать передатчик о своих возможностях, количество пакетов, которых он не досчитался и т.д. В таком канале используется RTCP-протокол. Формат пакетов передаваемых в этом канале определяется в RFC 3605. По этому каналу передаётся сравнительно немного данных 200..300 байт в секунду, поэтому в целом, его наличие необременительно. Номер порта, на который отправляются RTCP-пакеты должен быть нечетным и на единицу больше номера порта, с которого приходит RTP-поток. В нашем примере мы не будем использовать этот канал, так как возможности приёмника и канала заведомо превышают наши, пока скромные, потребности.
В нашей программе схема передачи данных, в отличие от схемы предыдущего примера, будет разделена на две части: на передающий тракт и приемный тракт. Для каждой части мы сделаем свой источник тактов, как показано на заглавной картинке.
Односторонняя связь между ними будет осуществляться с помощью RTP-протокола. В данном примере нам не потребуется внешняя сеть, так как и передатчик и приёмник будут располагаться на одном компьютере — пакеты будут ходить у него внутри.
Для установления RTP-потока в медиастримере используются два фильтра: MS_RTP_SEND и MS_RTP_RECV. Первый выполняет передачу второй прием RTP-потока. Чтобы эти фильтры начали работать, им нужно передать указатель на объект RTP-сессии, которая может выполнять как преобразование потока блоков данных в поток RTP-пакетов так и выполнять обратное действие. Поскольку внутренний формат данных медиастримера не совпадает с форматом данных RTP-пакета, то перед передачей данных в MS_RTP_SEND нужно использовать фильтр конвертера (encoder), который преобразует 16-битные отсчеты звукового сигнала в восьмибитные, кодированные по u-закону (мю-закону). На приемной стороне обратную функцию выполняет фильтр decoder.
Ниже приведен текст программы, реализующей схему показанную на рисунке (символы # перед директивами include убран, не забудьте их поставить):
/* Файл mstest6.c Имитатор пульта управления и приемника. */
#include <mediastreamer2/msfilter.h>
#include <mediastreamer2/msticker.h>
#include <mediastreamer2/dtmfgen.h>
#include <mediastreamer2/mssndcard.h>
#include <mediastreamer2/msvolume.h>
#include <mediastreamer2/mstonedetector.h>
#include <mediastreamer2/msrtp.h>
#include <ortp/rtpsession.h>
#include <ortp/payloadtype.h>
/* Подключаем заголовочный файл с функциями управления событиями
* медиастримера.*/
include <mediastreamer2/mseventqueue.h>
#define PCMU 0
/* Функция обратного вызова, она будет вызвана фильтром, как только он
обнаружит совпадение характеристик входного сигнала с заданными. */
static void tone_detected_cb(void *data, MSFilter *f, unsigned int event_id,
MSToneDetectorEvent *ev)
{
printf("Принята команда: %s\n", ev->tone_name);
}
/*----------------------------------------------------------------------------*/
/* Функция регистрации типов полезных нагрузок. */
void register_payloads(void)
{
/*Регистрируем типы нагрузок в таблице профилей. Позднее, по индексу
взятому из заголовка RTP-пакета из этой таблицы будут извлекаться
параметры нагрузки, необходимые для декодирования данных пакета. */
rtp_profile_set_payload (&av_profile, PCMU, &payload_type_pcm8000);
}
/*----------------------------------------------------------------------------*/
/* Эта функция создана из функции create_duplex_rtpsession() в audiostream.c
медиастримера2. */
static RtpSession *
create_rtpsession (int loc_rtp_port, int loc_rtcp_port,
bool_t ipv6, RtpSessionMode mode)
{
RtpSession *rtpr;
rtpr = rtp_session_new ((int) mode);
rtp_session_set_scheduling_mode (rtpr, 0);
rtp_session_set_blocking_mode (rtpr, 0);
rtp_session_enable_adaptive_jitter_compensation (rtpr, TRUE);
rtp_session_set_symmetric_rtp (rtpr, TRUE);
rtp_session_set_local_addr (rtpr, ipv6 ? "::" : "0.0.0.0", loc_rtp_port,
loc_rtcp_port);
rtp_session_signal_connect (rtpr, "timestamp_jump",
(RtpCallback) rtp_session_resync, 0);
rtp_session_signal_connect (rtpr, "ssrc_changed",
(RtpCallback) rtp_session_resync, 0);
rtp_session_set_ssrc_changed_threshold (rtpr, 0);
rtp_session_set_send_payload_type(rtpr, PCMU);
/* По умолчанию выключаем RTCP-сессию, так как наш пульт не будет использовать её. */
rtp_session_enable_rtcp (rtpr, FALSE);
return rtpr;
}
/*----------------------------------------------------------------------------*/
int main()
{
ms_init();
/* Создаем экземпляры фильтров. */
MSFilter *voidsource = ms_filter_new(MS_VOID_SOURCE_ID);
MSFilter *dtmfgen = ms_filter_new(MS_DTMF_GEN_ID);
MSFilter *volume = ms_filter_new(MS_VOLUME_ID);
MSSndCard *card_playback =
ms_snd_card_manager_get_default_card(ms_snd_card_manager_get());
MSFilter *snd_card_write = ms_snd_card_create_writer(card_playback);
MSFilter *detector = ms_filter_new(MS_TONE_DETECTOR_ID);
/* Очищаем массив находящийся внутри детектора тонов, он описывает
* особые приметы разыскиваемых сигналов.*/
ms_filter_call_method(detector, MS_TONE_DETECTOR_CLEAR_SCANS, 0);
/* Подключаем к фильтру функцию обратного вызова. */
ms_filter_set_notify_callback(detector,
(MSFilterNotifyFunc)tone_detected_cb, NULL);
/* Создаем массив, каждый элемент которого описывает характеристику
* одного из тонов, который требуется обнаруживать: Текстовое имя
* данного элемента, частота в герцах, длительность в миллисекундах,
* минимальный уровень относительно 0,775В. */
MSToneDetectorDef scan[6]=
{
{"V+",440, 100, 0.1}, /* Команда "Увеличить громкость". */
{"V-",540, 100, 0.1}, /* Команда "Уменьшить громкость". */
{"C+",640, 100, 0.1}, /* Команда "Увеличить номер канала". */
{"C-",740, 100, 0.1}, /* Команда "Уменьшить номер канала". */
{"ON",840, 100, 0.1}, /* Команда "Включить телевизор". */
{"OFF", 940, 100, 0.1}/* Команда "Выключить телевизор". */
};
/* Передаем "приметы" сигналов детектор тонов. */
int i;
for (i = 0; i < 6; i++)
{
ms_filter_call_method(detector, MS_TONE_DETECTOR_ADD_SCAN,
&scan[i]);
}
/* Создаем фильтры кодера и декодера */
MSFilter *encoder = ms_filter_create_encoder("PCMU");
MSFilter *decoder=ms_filter_create_decoder("PCMU");
/* Регистрируем типы нагрузки. */
register_payloads();
/* Создаем RTP-сессию передатчика. */
RtpSession *tx_rtp_session = create_rtpsession (8010, 8011, FALSE, RTP_SESSION_SENDONLY);
rtp_session_set_remote_addr_and_port(tx_rtp_session,"127.0.0.1", 7010, 7011);
rtp_session_set_send_payload_type(tx_rtp_session, PCMU);
MSFilter *rtpsend = ms_filter_new(MS_RTP_SEND_ID);
ms_filter_call_method(rtpsend, MS_RTP_SEND_SET_SESSION, tx_rtp_session);
/* Создаем RTP-сессию приемника. */
MSFilter *rtprecv = ms_filter_new(MS_RTP_RECV_ID);
RtpSession *rx_rtp_session = create_rtpsession (7010, 7011, FALSE, RTP_SESSION_RECVONLY);
ms_filter_call_method(rtprecv, MS_RTP_RECV_SET_SESSION, rx_rtp_session);
/* Создаем источники тактов - тикеры. */
MSTicker *ticker_tx = ms_ticker_new();
MSTicker *ticker_rx = ms_ticker_new();
/* Соединяем фильтры передатчика. */
ms_filter_link(voidsource, 0, dtmfgen, 0);
ms_filter_link(dtmfgen, 0, volume, 0);
ms_filter_link(volume, 0, encoder, 0);
ms_filter_link(encoder, 0, rtpsend, 0);
/* Соединяем фильтры приёмника. */
ms_filter_link(rtprecv, 0, decoder, 0);
ms_filter_link(decoder, 0, detector, 0);
ms_filter_link(detector, 0, snd_card_write, 0);
/* Подключаем источник тактов. */
ms_ticker_attach(ticker_tx, voidsource);
ms_ticker_attach(ticker_rx, rtprecv);
/* Настраиваем структуру, управляющую выходным сигналом генератора. */
MSDtmfGenCustomTone dtmf_cfg;
dtmf_cfg.tone_name[0] = 0;
dtmf_cfg.duration = 1000;
dtmf_cfg.frequencies[0] = 440;
/* Будем генерировать один тон, частоту второго тона установим в 0. */
dtmf_cfg.frequencies[1] = 0;
dtmf_cfg.amplitude = 1.0;
dtmf_cfg.interval = 0.;
dtmf_cfg.repeat_count = 0.;
/* Организуем цикл сканирования нажатых клавиш. Ввод нуля завершает
* цикл и работу программы. */
char key='9';
printf("Нажмите клавишу команды, затем ввод.\n"
"Для завершения программы введите 0.\n");
while(key != '0')
{
key = getchar();
if ((key >= 49) && (key <= 54))
{
printf("Отправлена команда: %c\n", key);
/* Устанавливаем частоту генератора в соответствии с
* кодом нажатой клавиши. */
dtmf_cfg.frequencies[0] = 440 + 100*(key-49);
/* Включаем звуковой генератор c обновленной частотой. */
ms_filter_call_method(dtmfgen, MS_DTMF_GEN_PLAY_CUSTOM,
(void*)&dtmf_cfg);
}
/* Укладываем тред в спячку на 20мс, чтобы другие треды
* приложения получили время на работу. */
ms_usleep(20000);
}
}
Компилируем, запускаем. Программа будет работать как в прошлом примере, но при этом данные будут передаваться через RTP-поток.
В следующей статье мы разделим эту программу на два независимых приложения — приемник и передатчик и запустим их в разных терминалах. Параллельно научимся анализировать RTP-пакеты с помощью программы TShark.