探索 Mediastreamer2 VoIP 引擎。 第 9 部分

文章素材取自我 禅频道.

双工对讲

探索 Mediastreamer2 VoIP 引擎。 第 9 部分

在最后 文章 一款双工对讲机已经发布,我们将在这一款中实现它。

该图如标题图所示。 较低的滤波器链形成从声卡开始的传输路径。 它提供来自麦克风的信号样本。 默认情况下,这种情况发生的速率为每秒 8000 个样本。 媒体流音频过滤器使用的数据位深度是 16 位(这并不重要;如果您愿意,您可以编写适用于更高位深度的过滤器)。 数据被分为 160 个样本的块。 因此,每个块的大小为 320 字节。 接下来,我们将数据提供给生成器的输入,生成器关闭时对数据是“透明”的。 我添加了它,以防您在调试过程中厌倦了对着麦克风说话 - 您可以使用发生器用音调信号“射击”路径。

在发生器之后,信号进入编码器,编码器根据 µ 律(G.16 标准)将 711 位样本转换为 XNUMX 位样本。 在编码器的输出处,我们已经有了一半大小的数据块。 一般来说,如果不需要节省流量,我们可以不压缩地传输数据。 但在这里使用编码器很有用,因为 Wireshark 只能在根据 µ 律或 a 律压缩时从 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

添加评论