Entdecken Sie die VoIP-Engine von Mediastreamer2. Teil 6

Das Material des Artikels stammt von mir Zen-Kanal.

Übertragung eines Audiosignals per RTP-Stream

Entdecken Sie die VoIP-Engine von Mediastreamer2. Teil 6

In der Vergangenheit Artikel Wir haben eine Fernbedienungsschaltung aus einem Tongenerator und einem Tondetektor zusammengestellt, die im selben Programm arbeiten. In diesem Artikel erfahren Sie, wie Sie das RTP-Protokoll (RFC 3550 - RTP: Ein Transportprotokoll für Echtzeitanwendungen) zum Empfangen/Senden eines Audiosignals über ein Ethernet-Netzwerk.

RTP-Protokoll (Echtzeitprotokoll) bedeutet übersetzt Echtzeitprotokoll und wird zur Übertragung von Audio, Video, Daten und allem, was eine Übertragung in Echtzeit erfordert, verwendet. Nehmen wir als Beispiel ein Audiosignal. Die Flexibilität des Protokolls ermöglicht die Übertragung eines Audiosignals mit einer vorgegebenen Qualität.

Die Übertragung erfolgt mittels UDP-Paketen, sodass Paketverluste bei der Übertragung durchaus akzeptabel sind. Jedes Paket enthält einen speziellen RTP-Header und einen Datenblock des übertragenen Signals. Der Header enthält eine zufällig ausgewählte Signalquellenkennung, Informationen über die Art des übertragenen Signals und eine eindeutige Paketsequenznummer, damit die Pakete beim Dekodieren in der richtigen Reihenfolge angeordnet werden können, unabhängig von der Reihenfolge, in der sie vom gesendet wurden Netzwerk. Der Header kann auch zusätzliche Informationen enthalten, die sogenannte Erweiterung, die es ermöglicht, den Header für die Verwendung in einer bestimmten Anwendungsaufgabe anzupassen.

Der Datenblock enthält die Nutzlast des Pakets. Die interne Organisation des Inhalts hängt von der Art der Last ab, es kann sich um Samples eines Monosignals, eines Stereosignals, einer Videobildzeile usw. handeln.

Der Lasttyp wird durch eine 3551-Bit-Zahl angezeigt. Empfehlung RFCXNUMX (RTP-Profil für Audio- und Videokonferenzen mit minimaler Kontrolle) legt mehrere Belastungsarten fest; die entsprechende Tabelle enthält eine Beschreibung der Belastungsarten und die Bedeutung der Codes, mit denen sie bezeichnet werden. Einige Codes sind nicht streng an irgendeine Art von Last gebunden; sie können zur Bezeichnung einer beliebigen Last verwendet werden.

Die Größe eines Datenblocks wird oben durch die maximale Paketgröße begrenzt, die in einem bestimmten Netzwerk ohne Segmentierung übertragen werden kann (MTU-Parameter). Im Allgemeinen sind dies nicht mehr als 1500 Bytes. Um also die pro Sekunde übertragene Datenmenge zu erhöhen, können Sie die Paketgröße bis zu einem bestimmten Punkt erhöhen und müssen dann die Häufigkeit des Paketversands erhöhen. Bei einem Medienstreamer ist dies eine konfigurierbare Einstellung. Standardmäßig sind es 50 Hz, d.h. 50 Pakete pro Sekunde. Wir nennen die Folge der übertragenen RTP-Pakete einen RTP-Stream.

Um mit der Datenübertragung zwischen der Quelle und dem Empfänger zu beginnen, reicht es aus, dass der Sender die IP-Adresse des Empfängers und die Portnummer kennt, die er zum Empfangen verwendet. Diese. Ohne vorherige Eingriffe beginnt die Quelle mit der Übertragung von Daten und der Empfänger wiederum ist bereit, diese sofort zu empfangen und zu verarbeiten. Laut Standard muss die Portnummer, die zum Senden oder Empfangen eines RTP-Streams verwendet wird, gerade sein.

In Situationen, in denen es unmöglich ist, die Adresse des Empfängers im Voraus zu kennen, werden Server verwendet, auf denen die Empfänger ihre Adresse hinterlassen und der Sender sie unter Bezugnahme auf einen eindeutigen Namen des Empfängers anfordern kann.

In Fällen, in denen die Qualität des Kommunikationskanals oder die Fähigkeiten des Empfängers unbekannt sind, wird ein Rückmeldekanal eingerichtet, über den der Empfänger den Sender über seine Fähigkeiten, die Anzahl der verpassten Pakete usw. informieren kann. Dieser Kanal verwendet das RTCP-Protokoll. Das Format der in diesem Kanal übertragenen Pakete ist in RFC 3605 definiert. Über diesen Kanal werden relativ wenige Daten übertragen, 200 bis 300 Bytes pro Sekunde, sodass seine Anwesenheit im Allgemeinen keine Belastung darstellt. Die Portnummer, an die RTCP-Pakete gesendet werden, muss ungerade und um eins größer sein als die Portnummer, von der der RTP-Stream kommt. In unserem Beispiel werden wir diesen Kanal nicht verwenden, da die Fähigkeiten des Empfängers und des Kanals offensichtlich unsere bisher bescheidenen Bedürfnisse übersteigen.

In unserem Programm wird die Datenübertragungsschaltung im Gegensatz zum vorherigen Beispiel in zwei Teile unterteilt: einen Sendepfad und einen Empfangspfad. Für jeden Teil erstellen wir eine eigene Taktquelle, wie im Titelbild gezeigt.

Die unidirektionale Kommunikation zwischen ihnen erfolgt über das RTP-Protokoll. In diesem Beispiel benötigen wir kein externes Netzwerk, da sich sowohl der Sender als auch der Empfänger auf demselben Computer befinden – die Pakete werden darin übertragen.

Um einen RTP-Stream einzurichten, verwendet der Medienstreamer zwei Filter: MS_RTP_SEND und MS_RTP_RECV. Der erste sendet den zweiten und empfängt den RTP-Stream. Damit diese Filter funktionieren, müssen sie einen Zeiger auf ein RTP-Sitzungsobjekt übergeben, das entweder einen Datenblockstrom in einen RTP-Paketstrom umwandeln oder das Gegenteil bewirken kann. Da das interne Datenformat des Medienstreamers nicht mit dem Datenformat des RTP-Pakets übereinstimmt, müssen Sie vor der Übertragung der Daten an MS_RTP_SEND einen Encoderfilter verwenden, der 16-Bit-Audiosignalabtastwerte in XNUMX-Bit-codierte entsprechend umwandelt U-Gesetz (Mu-Gesetz). Auf der Empfangsseite übernimmt der Decoderfilter die entgegengesetzte Funktion.

Unten finden Sie den Text des Programms, das das in der Abbildung gezeigte Schema implementiert (die #-Symbole vor den Include-Anweisungen wurden entfernt, vergessen Sie nicht, sie einzufügen):

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

Wir kompilieren und führen es aus. Das Programm funktioniert wie im vorherigen Beispiel, die Daten werden jedoch über einen RTP-Stream übertragen.

Im nächsten Artikel werden wir dieses Programm in zwei unabhängige Anwendungen aufteilen – einen Empfänger und einen Sender – und diese in verschiedenen Terminals starten. Gleichzeitig lernen wir, wie man RTP-Pakete mit dem Programm TShark analysiert.

Source: habr.com

Kommentar hinzufügen