Про VoIP-системи та їх тестування

Усі статті, обговорення, новини про тестування — в одному місці. Підписуйтеся на QA DOU!

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

Статтю я намагався зробити цікавою не тільки колезі-телекомщику, а більш-менш широкому загалу технічних спеціалістів. Але друга частина все ж таки призначена більше для колег, котрi, можливо, знайдуть щось корисне і для себе.

Трохи про мене: в телефонії працюю з 2008 року, з акцентом саме на цифрову, але довелося працювати і з класичною, тобто аналоговою. Працював в різних компаніях, був дотичний до побудови телефонії для великої мережі піцерій в Києві та області, довгий час працював фрілансером (так, можна бути фрілансером і в телекомі) переважно на ринках Європи та США, в 2020 році отримав пропозицію роботи в ЦЕРНі (це там, де Великий Адронний Коллайдер та народився www), де зараз і працюю, роблячи зв’язок для вчених і не тільки.

Але перш ніж ми перейдемо до проблем та різних шляхів їх вирішення, скажу пару слів про «а що це взагалі таке».

Про що ми говоримо

VoIP — це Voice over IP, тобто передача голосу через IP-мережі.

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

Сучасні системи зв’язку мало чим відрізняються від цих, хіба що панянок замінили бездушні автомати. Джерело

І переважно залишалась такою до 1950-х. І повільно «цифровізувалась», але навіть тепер аналоговим телефоном у бабусі в квартирі нікого не здивуєш. Як додаток до цифровізації, телефонні мережі навчились передавати не лише голос, а й дані, зокрема відео, але це все будувалось паралельно з мережами, які ми зараз знаємо як «пращурів інтернету». Можете уявити рівень легасі в таких системах, коли треба тягнути сумісність з системами, які робились в 70-ті. Та й заміна обладнання, особливо абонентського (той самий телефон у бабусі вдома) не те саме, що «спробуйте новішу версію Chrome».

Цей маленький історичний екскурс зроблений для того, щоб попередити можливі питання про «а чого все так, давайте переробимо все нормально». Неможливо вже переробити. Працюємо з тим, що є. Як з технологіями, так і з протоколами та підходами. Ситуація гірша, ніж з відмовою від IPv4, хоча тут можна сказати, що, наприклад 4G мережі, що вже є майже усюди, за дизайном IPv6-only.

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

Сигналізація — це спосіб сторін виклику домовитись про те, як виклик буде відбуватись. Тобто якщо ви набираєте на клавіатурі телефона номер та натискаєте «виклик», то це ще не розмова, це «запрошення» до розмови іншої сторони. Недарма в протоколі SIP, що зараз є стандартом де-факто в більшості систем VoIP, такий пакет-«запрошення» зветься INVITE. Так само коли ви кладете слухавку після розмови, то теж справа сигналізації сказати іншій стороні покласти трубку. Цей пакет зветься BYE.

Є ще один важливий для нас пакет — REGISTER. Це коли абонент сповіщає сервер, де він зараз знаходиться, реєструється на сервері, щоб той міг його знайти, якщо цього абонента викликають.

Доречі, LTE та 5G мережі теж використовують SIP (правда, розширений, але в базі — той же самий).

Медіа дані, точніше потік медіа даних, голос та/або відео, передається звичайним RTP протоколом (з шифруванням або без) через UDP (також можливо i через TCP, але це екзотика). Сучасні системи намагаються встановити медіа-канал між абонентами напряму, щоб мінімізувати навантаження на мережу.

В загальному вигляді виклик між двома абонентами виглядає так:

Класична «SIP-трапеція»

Ще раз наголошу, SIP тут тільки сигналізація, а голос ходить по RTP.

Ті, хто знайомий з webRTC, могли помітити щось знайоме, що не дивно, бо саме цей підхід використаний для створення цього протоколу. Тут мені можуть ще згадати про протокол SDP, але я заспокою, це теж з телефонії, в деяких пакетах SIP передає дані щодо медіа саме за допомогою SDP, а в webRTC протокол сигналізації може бути іншим, хоча ніхто не забороняє використовувати і SIP. Що реально з’явилось з приходом webRTC, то стек протоколів ICE, але то для пошуку оптимального шляху для медіа в умовах сучасних мереж.

Ще один термін, який нам знадобиться, це АТС або Автоматична Телефонна Станція. Або, як більш звично телеком-інженерам, PBX — Private Branch eXchange, те ж саме, тільки англійською. Таких АТСок різного розміру, форм та технологій існує та встановлено величезна кількість. Будь-яка організація більше ніж директор-секретар-менеджер встановлює такі системи для більш гнучкого контролю за телефонними дзвінками в компанії. Тут мені можуть сказати, що «все давно вирішується в email та в чатіках», я ж відповім, що телефонна розмова — це, перш за все, синхронний процес. Швидкість вирішення питання голосом або через відео набагато вища. Коли пожежа або аварія, зазвичай не пишуть в телегу або СМСку.

Хух, наче з загальною частиною все. Можна й кави випити. Та продовжувати.

Що нам робити з АТС та нащо

Сьогодні найбільш популярною АТСкою з відкритим кодом є Asterisk. Це софт, який дозволяє перетворити звичайний Linux сервер на повноцінну АТСку. Конфігурується вона за допомогою своєї, трохи езотеричної (для непідготовленого) мови програмування (прошу вибачення у колег за таке визначення).

Як і будь-яка інша складна система, АТСки потребують конфігурування. Колись це робили механічними перемикачами, в якийсь момент АТСки офісного рівня програмувались прямо з телефонних апаратів (ох і займало це часу, скажу я вам, можна було всю ніч просидіти разом з інструкцією та апаратом). Зараз практично всі АТСки програмуються з комп’ютера або через спеціальний софт, що надав виробник (в тому числі веб-інтерфейси), або через текстові конфігураційні файли в випадку того ж Asterisk.

Конфігурація АТС — це надання системі знання про абонентів та що робити, коли ці абоненти набирають номери. Робити ж системи за ці роки навчились багато. Але каву все одно чогось просять принести секретаря. Частіше саме телефоном.

Після того як інженер налаштував нумераційний план (тих самих абонентів), правила маршрутизації (що робити, коли абонент набрав номер) та супутні сервіси (ті самі повідомлення, що «ваш дзвінок дуже важливий для нас, очікуйте...») хотілось би пересвідчитись, що все працює, як і планувалось.

І ось тут починається тестування.

Класичне тестування систем телефонного зв’язку відбувається приблизно так:

І це не жарт. На фото нижче — частина мого робочого місця. Сюди ще не потрапили софтфони на комп’ютері та мобільні пристрої.

Зазвичай процес такий: вам приходить завдання типу «мені потрібно, щоб мій робочий телефон в неробочий час автоматом переадресовувся на телефон колеги на чергуванні». Зрозуміло, що такий функціонал в будь-якій пристойній системі вже є, але припустимо, що наша система тільки розвивається і це перший запит такого типу.

Сідаємо писати конфіги, інтеграції з календарем та таке інше. А потім беремо трубку і починаємо симулювати ситуації. Зрозуміло, що за тестами до користувача не підеш і починаєш рольові ігри з телефонами, що у тебе під рукою, де один телефон — користувач, другий — колега, третій — той, хто власне дзвонить користувачу і т. д.

І пішли сценарії тестування. Просто дзвінок в робочий час. Потім підкрутити календар та симулювати неробочий час, повторити дзвінок і пересвідчитись, що все працює як треба. Це два основних тести. Ще є багато різних сценаріїв навіть в цій простій ситуації, наприклад, куди відправляти дзвінок, коли колега на чергуванні не відповів, або куди відправляти повідомлення про такі дзвінки?

Або в першому тесті треба пересвідчитись, що дзвінок не просто працює, а обидві сторони ще й чують одна одну.

Як пересвідчитись, що все працює правильно

Мануальні тести в телефонії — наше всьо.

Думаю, мені тут не треба розповідати про те, що це не те, щоб погано, але погано :)

І для тестування подібних систем існує софт. Ну як, існує... частково.

Перший спосіб

Один з найпопулярніших застосунків в цій сфері — SIPP.

SIPP у всій красі

Чудовий софт з зовсім не чудовою системою конфігурації, яка починалась з простого XML, що описував послідовніть пакетів та контролю відповідей, а закінчився наче тим самим XML, але з езотеричними умовними конструкціями. Плюс, софт розроблявся саме для тестування навантаження сигналізації, медіа-можливості у нього дуже скромні.

Тобто це штука для тестування саме SIP-протоколу на доволі низькому рівні, той же звичайний SIP-телефон повністю на ньому реалізувати неможливо. Xто скаже, що можна, я хотів би побачити сценарій «зареєструвався — чекаю на виклик, відповідаючи на OPTIONS».

Але коли заходить питання про тестування навантаження — тут цій штуці немає рівних і вона по праву посідає почесне місце в інструментах тестування.

Якщо когось зацікав цей інструмент, найкращий спосіб його вивчити — через приклади.

Екзотичний спосіб

Наступний спосіб може видатись трохи дивним, але він робочий.

Вже згадана вище Asterisk або інша популярна АТСка FreeSWITCH може «імітувати» звичайний SIP-телефон і, що приємно, не один. Також може керуватись через API. Але не повністю, частина конфігурації має бути надана в файлах, що при «звичайному» використанні не є проблемою.

Тобто тест — це така собі інструкція з налаштування тієї самої АТСки, а саме конфігурації телефонів, маршрутів, де описується поведінка дзвінка та робляться необхідні перевірки. Також не треба забувати про необхідність налаштування доступу для API, скрипта, що буде викликати дзвінки. Хтось скаже, що достатньо один раз написати «скелет», а потім його адаптувати. Так, це полегшить справу, але не сильно, саме через описання маршрутів. Плюс, є нетривіальні штуки типу «як перевірити неправильні реєстрації», що будуть змушувати дописувати до АТСки ще обвіси скриптів перевірки саме на такі ситуації.

В кінці один тест буде повноцінним проєктом з розгортання АТСки та обвісу навколо.

З іншого боку, цей підхід дозволить протестувати доволі складні сценарії, як, наприклад, коректність транскодингу DTMF з in-band в SIP-INFO (хто не зрозумів, що це таке і навіщо це треба — щаслива людина).

Майже робочий спосіб

Ще один спосіб — автоматизація консольних SIP-телефонів (так, є і такі), наприклад, pjsua, baresip, або linphone-cli.

Так виглядає pjsua. Рідна консолька

На жаль, в реальності ці консольні софтфони в поточному вигляді більше нагадують демо-проєкти для бібліотек (SDK), на яких вони побудовані. Конкретно тут я можу помилятись, але жодного успішного досвіду використання та управління через API саме консольними клієнтами у мене немає.

Але саме ці софтфони можуть показати наступний крок: якщо немає інструментів — створи сам. Благо, всі перелічені клієнти мають за собою доволі гарні, стабільні та підтримувані SDK.

DIY-спосіб

Cпосіб «візьми і зроби». Або пошукай, що вже є на GitHub. Мені сподобався один з таких проєктів — VoIP patrol, який написаний саме з цією ідеєю — мати на столі безліч телефонних апаратів, робити та отримувати на них дзвінки з контролем параметрів.

Трохи погравшись з основним проєктом, я дійшов висновку, що в тому варіанті, як він є, саме для моїх задач, його замало. Але допрацювання не здавались чимось складним. Тому типовий для цих задач шлях — форк + доробка під себе. Ось і згодились знання С++ (синтаксис не лякає — і то добре), що я колись вивчав в інституті.

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

Що ж може цей проєкт?

Найкращій спосіб дізнатись — подивитись відео від автора, де він доволі докладно все пояснює в деталях.

Але так було б нецікаво, просто роздати посилання і піти :)

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

Конфігурація здійснюється за допомогою XML (так, знову він), де просто описуються кроки, які система має пройти.

Скажу одразу: для людини, що не займається телефонією, почнуть виникати питання, але відповіді на них — практика з VoIP та знання SIP-протоколу.

Наведу простий приклад з документації, просто щоб ви мали уявлення, про що йдеться.

<config>
  <actions>
    <action type="call" label="us-east-va"
            transport="tls"
            expected_cause_code="200"
            caller="[email protected]"
            callee="[email protected]"
            to_uri="[email protected]"
            max_duration="20" hangup="16"
            auth_username="VP_ENV_USERNAME"
            password="VP_ENV_PASSWORD"
            realm="target.com"
            rtp_stats="true"
    >
    </action>
    <!-- note: param value starting with VP_ENV_ will be replaced by environment variables -->
    <action type="wait" complete="true"/>
  </actions>
</config>

Тут описується дзвінок, де транспортом для SIP виступає TLS, дзвінок на номер 12012665228 (to_uri + callee), на нього отримана відповідь (expected_cause_code). Також для аутентифікації дзвінка на сервері використовується логін та пароль (auth_username та password). Після завершення дзвінка буде надана статистика по медіа-каналу, отримана через RTCP (rtp_stats).

Наче все добре, але є одне але. Ця програма може виконати лише 1 сценарій за 1 виконання. Тобто запустили з XML-сценарієм, отримали звіт, все. Не дуже зручно.

Мій спосіб

Саме тому народився VOLTS — Voip Open Linear Test Suite, що є надбудовою та запускалкою VoIP Patrol з можливістю запускати більше ніж 1 тест та аналізувати отримані звіти в вигляді простої таблички.

Самі сценарії тестування є тими ж самими XML-файлами від VoIP patrol, але з можливістю використовувати Jinja2 як шаблонізатор. Зроблено це, щоб можна було використовувати одних і тих самих абонентів для багатьох тестів.

Система є зв’язкою з трьох docker-контейнерів, кожен з яких запускається один за одним.

Блок-схема роботи VOLTS

Перший контейнер — то простий скрипт на пітоні, що перетворює шаблонізовані сценарії в реальні сценарії VoIP patrol.

Як приклад реального сценарію можна розібрати такий:

config.yaml

global:
  domain:     'mypbx.com'
  transport:  'udp'
accounts:
  '88881':
    username:       '88881'
    auth_username:  '88881'
    password:       'wrong_password_here'

01-expect-fail-on-register.xml

<config>
   <actions>
    	<action type="register" label="Register {{ a.88881.label }} fails"
        	transport="{{ c.transport }}"
        	account="{{ a.88881.label }}"
        	username="{{ a.88881.username }}"
        	auth_username="{{ a.88881.auth_username }}"
        	password="{{ a.88881.password }}"
        	registrar="{{ c.domain }}"
        	<!-- Ми очікуємо 407й код, але ваш сервер може повертати інший. -->
        	expected_cause_code="407"
    	/>
    	<action type="wait" complete="true" ms="2000"/>
   </actions>
</config>

У цьому сценарії описується тест реєстрації з неправильними ідентифікаційними даними. В принципі, перетворення зв’язки YAML + XML шаблон в XML-файл з реальними даними доволі очевидний, єдине, що я зробив для скорочення, це global структура в YAML перетворюється на c (від config) в XML-шаблоні, а відповідно accounts на a просто для скорочення.

Результат буде такий:

01-expect-fail-on-register.xml

<config>
   <actions>
    	<action type="register" label="Register 88881 fails"
        	transport="udp"
        	account="88881"
        	username="88881"
        	auth_username="88881"
        	password="wrong_password_here"
        	registrar="mypbx.com"
        	expected_cause_code="407"
    	/>
    	<action type="wait" complete="true" ms="2000"/>
   </actions>
</config>

Саме цей файл передається наступному контейнеру, що запускає VoIP patrol і там виконуються тести.

У результаті проходження тесту буде сформована строка в JSON-форматі, що проаналізується наступним контейнером, і він вже намалює табличка типу:

Якщо у нас тестів багато, то тести в контейнері будуть запускатись один за одним, власне тому і Linear в назві. Так, є варіант запускати тести одночасно, але тоді для кожного тесту буде треба створювати окремий набір абонентів.

Та й не прийнято в телефонії поспішати.

Коли файлів зі сценаріями багато, то табличка буде виглядати, відповідно, більше як тут:

Тепер у нас є колекція регресивних тестів (не впевнений за формулювання), тобто ті, що показують, що з новим функціоналом старий ще працює. Ну і можна перейти на TDD, що я вже і зробив. Спочатку тест, потім — функціонал.

Ну й наведу приклад сценарію, про який казав, що він неможливий для SIPP, а саме — імітація телефонного апарату. Зареєструвався, чекає на дзвінок. Для простоти покажу сам сценарій без конфігу:

02-register-and-wait-for-call.xml

<config>
	<actions>
    	<action type="codec" disable="all"/>
    	<action type="codec" enable="pcma" priority="250"/>
    	<action type="codec" enable="pcmu" priority="249"/>
    	<action type="codec" enable="opus" priority="248"/>
<!-- Реєструємо абонента 90002 -->
    	<action type="register" label="Register {{ a.90002.username }}"
        	transport="{{ a.90002.transport }}"
        	account="{{ a.90002.username }}"
        	username="{{ a.90002.label }}"
        	auth_username="{{ a.90002.username }}"
        	password="{{ a.90002.password }}"
        	registrar="{{ c.domain }}"
        	realm="{{ c.domain }}"
        	expected_cause_code="200"
        	srtp="{{ a.90002.srtp }}"
    	/>
<!-- Чекаємо на успішне завершення процесу реєстрації але не більше 2 сек -->
    	<action type="wait" complete="true" ms="2000"/>
<!-- Описуємо, що ми повинні отримати дзвінок саме на попередньо зареєстрованого абонента -->
    	<action type="accept" label="Receive call on {{ a.90002.username }} from {{ a.90001.label }}"
        	call_count="1"
        	match_account="{{ a.90002.username }}"
        	hangup="10"
        	code="200" reason="OK"
        	transport="{{ a.90002.transport }}"
        	srtp="{{ a.90002.srtp }}"
        	play="{{ c.play_file }}">
<!-- Бонусом перевіряємо, що номер, абонента, що нам дзвонить - вірний. Тобто 90001 -->
        	<check-header name="From" regex="^.*<sip:{{ a.90001.label }}@.*$"/>
    	</action>
<!-- Власне дзвонимо на номер 90002 -->
    	<action type="call" label="Call {{ a.90001.label }} -> {{ a.90002.label }}"
        	transport="tls"
        	expected_cause_code="200"
        	caller="{{ a.90001.label }}@{{ c.domain }}"
        	callee="{{ a.90002.label }}@{{ c.domain }}"
        	from="sip:{{ a.90001.label }}@{{ c.domain }}"
        	to_uri="{{ a.90002.label }}@{{ c.domain }}"
        	max_duration="20" hangup="10"
        	auth_username="{{ a.90001.username }}"
        	password="{{ a.90001.password }}"
        	realm="{{ c.domain }}"
        	rtp_stats="true"
        	max_ring_duration="15"
        	srtp="{{ a.90001.srtp }}"
        	play="{{ c.play_file }}"
    	/>
<!-- Чекаємо на успішне завершення дзвінка але не більше 30 сек -->
    	<action type="wait" complete="true" ms="30000"/>
	</actions>
</config>

Це доволі базовий приклад, але він є основою для більш складних сценаріїв.

Тепер про погане. Що ця система не може, так це сконфігурувати АТСку, тому це — за вами. Але то є справедливо на момент написання статті.

Зараз у розробці перебуває функціонал інтеграції з базами даних, бо в моєму випадку інформація про маршрути та абоненти знаходиться саме там (Asterisk Realtime, для тих, хто в курсі). Це має дозволити перейти на більш «чисте» тестування, коли тестова система записує дані, робить тест та підчищає за собою.

Ще треба сказати, що як VoIP Patrol, так і VOLTS, підтримуються лише двома людьми (а VOLTS — взагалі одним мною) і писалось для вирішення конкретних задач конкретних людей, то кількість багів може когось вразити. Іноді написання нового тесту призводить до знаходження ще одного бага або дописування нового функціоналу. Тут як ніде я користувався правилом — «працює — не чіпай».

Ще додам, що система розробляється і тестується лише в Linux-середовищі просто тому, що це моя основна як робоча, так і домашня система.

Всьо

Це все, що я хотів сьогодні розповісти. Розумію, що для багатьох тема виявиться незнайомою та непрофільною, але, як на мене, цікаво іноді почитати чи послухати про те, що відбувається в речах, до яких всі ми звикли. Тобто що за магія ховається за звичайним телефонним апаратом.

Із задоволенням відповім на будь-які питання та сподіваюсь, що інформація була корисна.

P.S.: Ця стаття писалась під час російського вторгнення в Україну. Користуючись нагодою, хочу подякувати громадським зв’язківцям в національних та не дуже операторах, що забезпечили та продовжують забезпечувати зв’язок увесь цей час. Бо зв’язок — як повітря, допоки його не зіпсували, його не помічають.

Хлопці та дівчата, в ядрі та на лінії, на вежі та в каналізації — низький вам уклін. Дякую.

P.P.S.: На 18.07.2022 у VOLTS з’явився функціонал інтеграції з базами даних та проведена невеличка презентація.

👍ПодобаєтьсяСподобалось11
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Поучаствую в перекличке. Занимаюсь тестированием качества голоса на постоянной основе :) sevana.biz . Аудио анализаторы свои, SIP голос тестируется через захват трафика или через запись в pjsua (или что-то другое что можно прицепить к скриптам).

У мене теж є в планах додати інформацію про MOS, але треба більш детально прочитати ліцензію PESQ, а більше нічого, наскільки я знаю, з відкритим кодом, нема.
Про AQuA читав, але бачити чи працювати з нею не доводилось.

PESQ — да, нужна лицензия. ViSQOL тоже открыт и с лицензией попроще github.com/google/visqol.

Двачую. В двух компаниях где тестят телефонию свои фреймворки на pjsip

А можна трохи розповісти про це? Просто реально мало інфи та загальних підходів, тому й велосипеди свої доводиться писати.

Pjsip бібліотека на Сі, і поверх неї написан враппер на lua. Тобто, у нас на низькому рівні pjsip, а на високому рівні lua викликає методи бібліотеки. При цьому сам pjsip хендліт всі запити і управляє дзвінком, тобто ті ж OPTIONS або вибір кодеків та відповіді 200 ОК і АСК наприклад це все відбувається на стороні pjsip, а в самих тестах на lua вже просто пишемо виконати дзвінок, відповісти, зробити трансфер і т.п.
ну і це напевно не загальних підхід а ще один велосипед:)

Тут в цілому зрозуміло, voip_patrol то теж враппер, але на С++. Тобто ідея така ж сама.
Мені що цікаво, як ці тести організовані. Тобто чи це просто набір lua-скриптів, де все з 0 описується, ну, тобто в кожному скрипті робимо аккаунти, дзвінки окремо та обвішуємось коллбеками від pjsip, чи є якийсь більш високий рівень абстракції?
Ну і чи є інтеграції зі сторонніми системами? Наприклад з базою даних, або через PESQ перевіряти MOS?

Так, кожен тест в окремому скрипті, тестовi данi вilокремленi, якісь функції та асерти зі своєю логікою винесені в окремі функції на вищому рівні і використовуються в усіх тестах, і потім — before_test, створили аккаунт, зробили дзвінки, after-test, згенерували звіт junit на luaunit, дженкінс зробив репорт, відправив листи, ну i цi репорти звичайно можно якось далi аналiзувати, але репортів junit у дженкінсі нам достатньо. MOS не перевіряємо, але мабуть варто почати)
З цікавого — імплементований детектор медії, — акаунти шлють медію певної частоти і можуть детектувати частоти, які приходять до них і визначати та медія грає, чи не та.
В одній компанії детектор працював прямо за набором звуків, тобто можна було всілякі промти кодувати і була їх велика варіативність

Круто, було б цікаво подивитись.
Дякую.

Наскільки пам’ятаю, детекція залежить від кодеку. Наприклад, G726 навіть DTMF не передає стабільно.

Я так розумію, тут не про точну частоту, що вимагається від DTMF, а про «на ехо приходить та сама мелодія». Хоча це здогадки.

Підписатись на коментарі