Wir untersuchen die VoIP-Engine Mediastreamer2. Teil 9

Der Artikelinhalt stammt von meinem Zen-Kanal.

Duplex-Gesprächseinheit

Wir untersuchen die VoIP-Engine Mediastreamer2. Teil 9

Letzte Woche Artikel Eine Duplex-Gesprächseinheit wurde angekündigt, und in diesem werden wir sie umsetzen.

Das Schema ist im Titelbild dargestellt. Die untere Filterkette bildet den Sendepfad, der mit der Soundkarte beginnt. Sie gibt die Signalproben vom Mikrofon aus. Standardmäßig geschieht dies mit einer Frequenz von 8000 Proben pro Sekunde. Die Datenauflösung, die die Soundfilter des Media Streamers verwenden, beträgt 16 Bit (was nicht entscheidend ist, da bei Bedarf Filter geschrieben werden können, die mit höherer Auflösung arbeiten). Die Daten sind in Blöcke zu je 160 Proben gruppiert. Somit hat jeder Block eine Größe von 320 Byte. Anschließend leiten wir die Daten an den Generator weiter, der im ausgeschalteten Zustand "transparent" für die Daten ist. Ich habe ihn hinzugefügt, für den Fall, dass Sie beim Debuggen keine Lust haben, ins Mikrofon zu sprechen – Sie können den Generator verwenden, um den Pfad mit einem Tonsignal zu "durchschießen".

Nach dem Generator gelangt das Signal zum Encoder, der unsere 16-Bit-Proben nach µ-Gesetz (Standard G.711) in 8-Bit-Proben umwandelt. Am Ausgang des Encoders haben wir bereits einen Datenblock, der nur halb so groß ist. Im Allgemeinen können wir die Daten ohne Kompression übertragen, wenn wir den Traffic nicht sparen müssen. Aber hier ist es nützlich, den Encoder zu verwenden, da Wireshark den Ton aus dem RTP-Stream nur abspielen kann, wenn er gemäß µ-Gesetz oder a-Gesetz komprimiert ist.

Nach dem Encoder gelangen die verkleinerten Datenblöcke zum Filter rtpsend, der sie in RTP-Pakete einfügt, die erforderlichen Flags setzt und sie an den Media Streamer zum Versand über das Netzwerk in Form von UDP-Paketen weitergibt.

Die obere Filterkette bildet den Empfangspfad. Die RTP-Pakete, die der Media Streamer aus dem Netzwerk empfangen hat, gelangen zum Filter rtprecv, dessen Ausgang sie bereits als Datenblöcke erscheinen lässt, von denen jeder einem empfangenen Paket entspricht. Der Block enthält nur die Nutzdaten, die in dem vorherigen Artikel im Bild grün dargestellt wurden.

Anschließend gelangen die Blöcke zum Decoder-Filter, der die darin enthaltenen einstufigen Proben in lineare, 16-Bit-Proben umwandelt. Diese können dann von den Filtern des Media Streamers verarbeitet werden. In unserem Fall geben wir sie einfach an die Soundkarte weiter, um sie über die Lautsprecher Ihres Headsets wiederzugeben.

Jetzt kommen wir zur praktischen Implementierung. Dafür werden wir die Dateien des Empfängers und des Senders zusammenführen, die wir zuvor getrennt haben. Zuvor hatten wir feste Port- und Adressenkonfigurationen verwendet, aber jetzt muss das Programm in der Lage sein, die Einstellungen zu verwenden, die wir beim Start angeben. Dazu fügen wir die Funktion zur Verarbeitung von Befehlszeilenargumenten hinzu. Danach können wir die IP-Adresse und den Port des Gesprächsgeräts festlegen, mit dem wir eine Verbindung herstellen möchten.

Zuerst fügen wir der Anwendung eine Struktur hinzu, die ihre Einstellungen speichert:

struct _app_vars
{
  int  local_port;              
  int  remote_port;             
  char remote_addr[128];        
  MSDtmfGenCustomTone dtmf_cfg; 
};

typedef struct _app_vars app_vars;

Im Programm wird eine Struktur dieses Typs mit dem Namen vars deklariert.
Dann fügen wir die Funktion zum Parsen der Befehlszeilenargumente hinzu:

/* Функция преобразования аргументов командной строки в
* настройки программы. */
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]);
        }
    }
}

Das Parsen wird dazu führen, dass die Befehlszeilenargumente in die Felder der Struktur vars eingefügt werden. Die Hauptfunktion der Anwendung wird die Übertragung und den Empfang durch Filter zusammenführen. Nach dem Anschluss des Tickers wird die Steuerung an eine Endlosschleife übergeben, die, falls die Frequenz des Generators ungleich null war, den Testgenerator neu starten wird – damit er ohne Unterbrechung arbeitet.

Diese Neustarts sind für den Generator erforderlich, weil er aus irgendeinem Grund kein Signal von mehr als 16 Sekunden Dauer erzeugen kann. Dabei sollte beachtet werden, dass die Dauer durch eine 32-Bit-Zahl bestimmt wird.

Das gesamte Programm wird so aussehen:

/* Файл 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);
    }
}

Wir kompilieren. Danach kann das Programm auf zwei Computern gestartet werden. Oder auf einem, wie ich es jetzt tun werde. Wir starten TShark mit den folgenden Argumenten:

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

Wenn im Konsolenstart nur die Nachricht über den Beginn der Erfassung erscheint, ist das ein gutes Zeichen – das bedeutet, dass unser Port wahrscheinlich nicht von anderen Programmen belegt ist. In einem anderen Terminal starten wir ein Exemplar des Programms, das das "entfernte" Gesprächsgerät simuliert, indem wir ihm diese Portnummer angeben:

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

Wie aus dem Programmtext hervorgeht, wird standardmäßig die IP-Adresse 127.0.0.1 (localhost) verwendet.

In einem weiteren Terminal starten wir eine zweite Instanz des Programms, die ein lokales Gerät imitiert. Wir verwenden ein zusätzliches Argument, das die Arbeit des eingebauten Testgenerators erlaubt:

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

In diesem Moment sollten in der TShark-Konsole Pakete erscheinen, die in Richtung des 'remote' Geräts gesendet werden, und aus den Lautsprechern des Computers ertönt ein kontinuierliches Tönsignal.

Wenn alles wie beschrieben abgelaufen ist, starten wir die zweite Instanz des Programms neu, jedoch ohne den Schlüssel und das Argument '--gen 440'. Die Rolle des Generators übernehmen nun Sie. Danach können Sie ins Mikrofon reden, in den Lautsprechern oder Kopfhörern sollten Sie den entsprechenden Klang hören. Es kann sogar zu einem akustischen Self-Excitation kommen, verringern Sie die Lautstärke des Lautsprechers und der Effekt verschwindet.

Wenn Sie auf zwei Computern und sich nicht in den IP-Adressen verwirrt haben, wird das Ergebnis dasselbe sein – bidirektionale Sprachkommunikation in digitaler Qualität.

Im nächsten Artikel lernen wir, wie man eigene Filter – Plugins – schreibt. Mit diesem Wissen können Sie den Mediastreamer nicht nur für Audio und Video, sondern auch in anderen spezifischen Bereichen einsetzen.

Quelle: habr.com

60GB SSD 8Gb DDR4