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

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

Дуплексний переговорний пристрій

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

У минулій статті було анонсовано дуплексний переговорний пристрій, а в цій ми його зробимо.

Схема зображена на великому малюнку. Нижній ланцюг фільтрів утворює передавальний тракт, який починається зі звукової карти. Вона видає відліки сигналу з мікрофона. За умовчанням це відбувається у темпі 8000 відліків на секунду. Розрядність даних, яку використовують звукові фільтри медіастрімера - 16 біт (це не принципово, за бажання можна написати фільтри, які будуть працювати з більшою розрядністю). Дані згруповані в блоки 160 відліків. Таким чином, кожен блок має розмір 320 байт. Далі ми подаємо дані на вхід генератора, який у вимкненому стані "прозорий" для даних. Я його додав на той випадок, коли вам при налагодженні набридне говорити в мікрофон — ви зможете скористатися генератором, щоб прострілити тракт тональним сигналом.

Після генератора сигнал потрапляє на кодер, який перетворює наші 16-бітові відліки за µ-законом (стандарт G.711) у восьмибітні. На виході кодера ми маємо блок даних вдвічі меншого розміру. У загальному випадку ми можемо передавати дані без стиснення, якщо нам не потрібно економити трафік. Але тут корисно скористатися кодером, оскільки Wireshark може відтворювати звук з RTP-потоку тільки тоді, коли він стиснутий за µ-законом або а-законом.

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

Верхня ланцюг фільтрів утворює приймальний тракт, RTP-пакети, отримані медіастрімером з мережі, надходять у фільтр rtprecv, на виході якого вони з'являються вже у вигляді блоків даних, кожен з яких відповідає одному прийнятому пакету. Блок містить лише дані корисного навантаження, у попередній статті на ілюстрації вони були показані зеленим кольором.

Далі блоки надходять на фільтр декодера, який перетворює однобайтні відліки, що знаходяться в них, в лінійні, 16-бітові. Які вже можна обробляти фільтрами медіастрімера. У нашому випадку ми просто віддаємо їх на звукову карту для відтворення на динаміках вашої гарнітури.

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

Спочатку додамо до програми структуру, яка зберігатиме її налаштування:

struct _app_vars
{
  int  local_port;              /* Локальный порт. */
  int  remote_port;             /* Порт переговорного устройства на удаленном компьютере. */
  char remote_addr[128];        /* IP-адрес удаленного компьютера. */
  MSDtmfGenCustomTone dtmf_cfg; /* Настройки тестового сигнала генератора. */
};

typedef struct _app_vars app_vars;

У програмі буде оголошено структуру цього типу з ім'ям vars.
Потім додамо функцію аналізу аргументів командного рядка:

/* Функция преобразования аргументов командной строки в
* настройки программы. */
void  scan_args(int argc, char *argv[], app_vars *v)
{
    char i;
    for (i=0; i<argc; i++)
    {
        if (!strcmp(argv[i], "--help"))
        {
            char *p=argv[0]; p=p + 2;
            printf("  %s walkie talkienn", p);
            printf("--help      List of options.n");
            printf("--version   Version of application.n");
            printf("--addr      Remote abonent IP address string.n");
            printf("--port      Remote abonent port number.n");
            printf("--lport     Local port number.n");
            printf("--gen       Generator frequency.n");
            exit(0);
        }

        if (!strcmp(argv[i], "--version"))
        {
            printf("0.1n");
            exit(0);
        }

        if (!strcmp(argv[i], "--addr"))
        {
            strncpy(v->remote_addr, argv[i+1], 16);
            v->remote_addr[16]=0;
            printf("remote addr: %sn", v->remote_addr);
        }

        if (!strcmp(argv[i], "--port"))
        {
            v->remote_port=atoi(argv[i+1]);
            printf("remote port: %in", v->remote_port);
        }

        if (!strcmp(argv[i], "--lport"))
        {
            v->local_port=atoi(argv[i+1]);
            printf("local port : %in", v->local_port);
        }

        if (!strcmp(argv[i], "--gen"))
        {
            v -> dtmf_cfg.frequencies[0] = atoi(argv[i+1]);
                printf("gen freq : %in", v -> dtmf_cfg.frequencies[0]);
        }
    }
}

В результаті аналізу аргументи командного рядка будуть поміщені в поля структури vars. Головна функція програми буде збирати з фільтрів передавальний і приймальний тракти, після підключення тикера управління буде передано нескінченному циклу, який, якщо частота генератора була задана ненульовою, буде перезапускати тестовий генератор - щоб він працював без зупинки.

Ці перезапуски будуть потрібні генератору через його особливості побудови, чому він не може видавати сигнал тривалістю більше 16 секунд. При цьому слід зауважити, що тривалість у нього визначається 32-бітовим числом.

Програма цілком виглядатиме так:

/* Файл mstest8.c Имитатор переговорного устройства. */

#include <mediastreamer2/mssndcard.h>
#include <mediastreamer2/dtmfgen.h>
#include <mediastreamer2/msrtp.h>

/* Подключаем файл общих функций. */
#include "mstest_common.c"

/*----------------------------------------------------------*/
struct _app_vars
{
    int  local_port;              /* Локальный порт. */
    int  remote_port;             /* Порт переговорного устройства на удаленном компьютере. */
    char remote_addr[128];        /* IP-адрес удаленного компьютера. */
    MSDtmfGenCustomTone dtmf_cfg; /* Настройки тестового сигнала генератора. */
};

typedef struct _app_vars app_vars;

/*----------------------------------------------------------*/
/* Создаем дуплексную RTP-сессию. */
RtpSession* create_duplex_rtp_session(app_vars v)
{
    RtpSession *session = create_rtpsession (v.local_port, v.local_port + 1, FALSE, RTP_SESSION_SENDRECV);
    rtp_session_set_remote_addr_and_port(session, v.remote_addr, v.remote_port, v.remote_port + 1);
    rtp_session_set_send_payload_type(session, PCMU);
    return session;
}

/*----------------------------------------------------------*/
/* Функция преобразования аргументов командной строки в
* настройки программы. */
void  scan_args(int argc, char *argv[], app_vars *v)
{
    char i;
    for (i=0; i<argc; i++)
    {
        if (!strcmp(argv[i], "--help"))
        {
            char *p=argv[0]; p=p + 2;
            printf("  %s walkie talkienn", p);
            printf("--help      List of options.n");
            printf("--version   Version of application.n");
            printf("--addr      Remote abonent IP address string.n");
            printf("--port      Remote abonent port number.n");
            printf("--lport     Local port number.n");
            printf("--gen       Generator frequency.n");
            exit(0);
        }

        if (!strcmp(argv[i], "--version"))
        {
            printf("0.1n");
            exit(0);
        }

        if (!strcmp(argv[i], "--addr"))
        {
            strncpy(v->remote_addr, argv[i+1], 16);
            v->remote_addr[16]=0;
            printf("remote addr: %sn", v->remote_addr);
        }

        if (!strcmp(argv[i], "--port"))
        {
            v->remote_port=atoi(argv[i+1]);
            printf("remote port: %in", v->remote_port);
        }

        if (!strcmp(argv[i], "--lport"))
        {
            v->local_port=atoi(argv[i+1]);
            printf("local port : %in", v->local_port);
        }

        if (!strcmp(argv[i], "--gen"))
        {
            v -> dtmf_cfg.frequencies[0] = atoi(argv[i+1]);
                printf("gen freq : %in", v -> dtmf_cfg.frequencies[0]);
        }
    }
}

/*----------------------------------------------------------*/
int main(int argc, char *argv[])
{
    /* Устанавливаем настройки по умолчанию. */
    app_vars vars={5004, 7010, "127.0.0.1", {0}};

    /* Устанавливаем настройки настройки программы в
     * соответствии с аргументами командной строки. */
    scan_args(argc, argv, &vars);

    ms_init();

    /* Создаем экземпляры фильтров передающего тракта. */
    MSSndCard *snd_card =
        ms_snd_card_manager_get_default_card(ms_snd_card_manager_get());
    MSFilter *snd_card_read = ms_snd_card_create_reader(snd_card);
    MSFilter *dtmfgen = ms_filter_new(MS_DTMF_GEN_ID);
    MSFilter *rtpsend = ms_filter_new(MS_RTP_SEND_ID);

    /* Создаем фильтр кодера. */
    MSFilter *encoder = ms_filter_create_encoder("PCMU");

    /* Регистрируем типы нагрузки. */
    register_payloads();

    /* Создаем дуплексную RTP-сессию. */
    RtpSession* rtp_session= create_duplex_rtp_session(vars);
    ms_filter_call_method(rtpsend, MS_RTP_SEND_SET_SESSION, rtp_session);

    /* Соединяем фильтры передатчика. */
    ms_filter_link(snd_card_read, 0, dtmfgen, 0);
    ms_filter_link(dtmfgen, 0, encoder, 0);
    ms_filter_link(encoder, 0, rtpsend, 0);

    /* Создаем фильтры приемного тракта. */
    MSFilter *rtprecv = ms_filter_new(MS_RTP_RECV_ID);
    ms_filter_call_method(rtprecv, MS_RTP_RECV_SET_SESSION, rtp_session);

    /* Создаем фильтр декодера, */
    MSFilter *decoder=ms_filter_create_decoder("PCMU");

    /* Создаем фильтр звуковой карты. */
    MSFilter *snd_card_write = ms_snd_card_create_writer(snd_card);

    /* Соединяем фильтры приёмного тракта. */
    ms_filter_link(rtprecv, 0, decoder, 0);
    ms_filter_link(decoder, 0,  snd_card_write, 0);

    /* Создаем источник тактов - тикер. */
    MSTicker *ticker = ms_ticker_new();

    /* Подключаем источник тактов. */
    ms_ticker_attach(ticker, snd_card_read);
    ms_ticker_attach(ticker, rtprecv);

    /* Если настройка частоты генератора отлична от нуля, то запускаем генератор. */   
    if (vars.dtmf_cfg.frequencies[0])
    {
        /* Настраиваем структуру, управляющую выходным сигналом генератора. */
        vars.dtmf_cfg.duration = 10000;
        vars.dtmf_cfg.amplitude = 1.0;
    }

    /* Организуем цикл перезапуска генератора. */
    while(TRUE)
    {
        if(vars.dtmf_cfg.frequencies[0])
        {
            /* Включаем звуковой генератор. */
            ms_filter_call_method(dtmfgen, MS_DTMF_GEN_PLAY_CUSTOM,
                    (void*)&vars.dtmf_cfg);
        }
        /* Укладываем тред в спячку на 20мс, чтобы другие треды
         * приложения получили время на работу. */
        ms_usleep(20000);
    }
}

Компілюємо. Далі програму можна запустити на двох комп'ютерах. Або на одному, як я робитиму зараз. Запускаємо TShark з наступними аргументами:

$ sudo tshark -i lo -f "udp dst port 7010" -P -V -O RTP -o rtp.heuristic_rtp:TRUE -x

Якщо поле запуску в консолі з'явиться тільки повідомлення про початок захоплення, це добрий знак - значить наш порт швидше за все не зайнятий іншими програмами. В іншому терміналі запускаємо екземпляр програми, який імітуватиме "віддалений" переговорний пристрій, вказавши йому цей номер порту:

$ ./mstest8 --port 9010 --lport 7010

Як видно з тексту програми, за промовчанням використовується IP-адреса 127.0.0.1 (локальна петля).

Ще в одному терміналі запускаємо другий екземпляр програми, який імітує локальний пристрій. Використовуємо додатковий аргумент, який дозволяє роботу вбудованого тестового генератора:

$ ./mstest8  --port 7010 --lport 9010 --gen 440

У цей момент, в консолі з TShark повинні почати миготіти пакети, що передаються у бік "віддаленого" пристрою, а з динаміка комп'ютера пролунає безперервний тональний сигнал.

Якщо все сталося як за писаним, то перезапускаємо другий екземпляр програми, але вже без ключа та аргументу "-gen 440". Роль генератора тепер виконуватимете ви. Після цього можна пошуміти у мікрофон, у динаміці чи навушниках ви повинні почути відповідний звук. Можливо навіть виникне акустичне самозбудження, зменште гучність динаміка і ефект зникне.

Якщо ви запускали на двох комп'ютерах і не заплуталися в IP-адресах, то на вас чекає той же результат — двосторонній мовний зв'язок цифрової якості.

У наступній статті ми навчимося писати свої власні фільтри — плагіни, завдяки цій навичці ви зможете застосувати медіастрімер не тільки для звуку та відео, а й у якійсь іншій специфічній галузі.

Джерело: habr.com

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