Вивчаємо VoIP-движок Mediastreamer2. Частина 6

Матеріал статті взято з мого дзен-каналу.

Передача звукового сигналу через RTP-потік

Вивчаємо VoIP-движок Mediastreamer2. Частина 6

У минулій статті ми зібрали схему дистанційного керування з генератора та детектора тональних сигналів, які працюють усередині однієї програми. У цій статті ми навчимося використовувати протокол RTP (RFC 3550 RTP: A Transport Protocol для Real-Time Applications) для прийому/передачі звукового сигналу через мережу Ethernet.

Протокол RTP (Протокол реального часу) у перекладі означає протокол реального часу, він використовується для передачі звуку, відео, даних, всього того, що вимагає передачі в режимі реального часу. Як приклад візьмемо звуковий сигнал. Гнучкість протоколу така, що дозволяє передавати звуковий сигнал із заздалегідь заданою якістю.

Передача виконується за допомогою UDP-пакетів, що означає, що при передачі цілком допускається втрата пакетів. У кожен пакет вкладається спеціальний RTP-заголовок і блок даних сигналу, що передається. У заголовку міститься випадково вибирається ідентифікатор джерела сигналу, інформація про тип сигналу, що передається, унікальний порядковий номер пакета, для того щоб пакети при декодуванні могли бути вибудовані в правильному порядку, незалежно від того в якому порядку їх доставила мережу. Заголовок також може містити додаткову інформацію, так зване розширення, яке дозволяє адаптувати заголовок до застосування конкретної прикладної задачі.

Блок даних містить корисне навантаження пакета. Внутрішня організація вмісту залежить від типу навантаження, це може бути відліки монофонічного сигналу, стереосигнал, рядок відео зображення тощо.

Тип навантаження позначається семибітним числом. Рекомендація RFC3551 (RTP Profile for Audio and Video ConferencesMinimal 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("Принята команда: %sn", 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("Отправлена команда: %cn", 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.

Джерело: habr.com

Додати коментар або відгук