
Передача звукового сигнала через 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.
