Пока одни люди думают о регулировании мессенджеров, другие люди разрабатывают распределенные мессенджеры. В предыдущей публикации рассматривалось использование API ядра мессенджера Tox на примере создания простого echo-бота. Разработка Tox не стоит на месте и 3-го ноября ядро Tox обогатилось новой подсистемой аудио и видео вызовов — ToxAV о которой я и хотел бы рассказать в данной публикации.

image


Введение


ToxAV основывается на кодеках Opus для аудио и VP8 для видео и их реализациях в библиотеках libopus и libvpx соответственно.

В качестве входного и выходного формата для аудио, API ToxAV использует формат PCM с 16-и битным отсчетом, поддержкой 1-го или 2-х каналов и частотой дискретизации 8, 12, 16, 24 и 48KHz. Длительность одного кадра данных должна составлять 2.5, 5, 10, 20, 40 или 60 ms (требование кодека opus).

В качестве входного и выходного формата для видео, API ToxAV использует кадры в формате YUV420 (он же IYUV и I420), которые относительно легко можно преобразовывать в более привычное пространство цветов RGB.

ToxAV не предусматривает каких-либо методов для захвата данных с аудио и видео устройств или их воспроизведения — все это оставлено на усмотрение разработчика приложения. Так, например, в клиенте µTox используется OpenAL для работы с аудио и v4l (video4Linux) для работы с видео устройствами. В клиенте qTox для работы с видео используется FFmpeg. В python (о котором косвенно пойдет речь ниже) мы можем использовать PyAudio для аудио и OpenCV для видео.

Общий цикл работы с ToxAV можно представить последовательно в виде:

  1. Инициализация ядра ToxCore (подробно описана в предыдущей публикации).
  2. Инициализация подсистемы ToxAV (toxav_new).
  3. Установка callback функций для обработки событий (toxav_callback_*) — обработчики событий вызываются из основного цикла (4) и в них обычно сосредоточена основная логика работы приложения.
  4. Основной рабочий цикл (toxav_iterate) и обработка событий.
  5. Пауза на время toxav_iteration_interval и возврат к предыдущему шагу.

Т.к. основным способом получения знаний о работе Tox API является чтение исходного кода (написанного на си), для упрощения дальнейшего изложения я воспользуюсь оберткой для языка python (pytoxcore на github). Для тех, кто не желает заниматься самостоятельной сборкой библиотеки из исходников, там же есть ссылки на готовые бинарные пакеты для распространенных дистрибутивов.

При использовании python-обертки получить справку по библиотеке можно следующим способом:

$ python
>>> from pytoxcore import ToxAV
>>> help(ToxAV)
class ToxAV(object)
 |  ToxAV object
...
 |  toxav_answer(...)
 |      toxav_answer(friend_number, audio_bit_rate, video_bit_rate)
 |      Accept an incoming call.
 |      If answering fails for any reason, the call will still be pending and it is possible to try and answer it later. Audio and video receiving are both enabled by default.
 |  
 |  toxav_audio_receive_frame_cb(...)
 |      toxav_audio_receive_frame_cb(friend_number, pcm, sample_count, channels, sampling_rate)
 |      This event is triggered when a audio data received.
...

Ниже остановимся на каждом шаге работы с API чуть более подробно.

Инициализация ToxAV


Для инициализации подсистемы ToxAV в качестве параметра вызова toxav_new используется экземпляр инициализированного ранее ядра ToxCore. Для одного экземпляра ToxCore может быть создан только один экземпляр ToxAV. В python обертке вызов toxav_new скрыт внутри конструктора и инициализация выглядит как:

from pytoxcore import ToxCore, ToxAV

class EchoBot(ToxCore):
...

class EchoAVBot(ToxAV):
    def __init__(self, core):
        super(EchoAVBot, self).__init__(core)
...

bot = EchoBot(options)
botav = EchoAVBot(bot)

Для уничтожения экземпляра ToxAV используется вызов toxav_kill или уничтожение экземпляра класса в python обертке, где вызов toxav_kill скрыт в деструкторе.

Установка callback функций


В python-обертке подключение к поддерживаемым callback функциям производится автоматически. Сами же обработчики могут являться методами наследника ToxAV и имеют суффикс *_cb. Во всех обработчиках одним из параметров является friend_number — целочисленный идентификатор друга из аналогичных методов ToxCore.

toxav_call_cb(friend_number, audio_enabled, video_enabled) — входящий звонок. В качестве аргументов дополнительно передаются флаги поддержки аудио и видео со стороны контакта. В дальнейшем контакт с отключенным аудио или видео потоками может их включить, что вызовет событие изменения состояния звонка. Звонок может быть принят вызовом toxav_answer, или отклонен вызовом toxav_call_control.

toxav_call_state_cb(friend_number, state) — изменение состояния звонка. В качестве аргумента передается состояние, которое является битовой маской из констант:

  • TOXAV_FRIEND_CALL_STATE_ERROR — таймаут передачи данных контакту. Данное состояние является последним состоянием для звонка (к этому моменту звонок завершен) и не комбинируется с другими состояниями.
  • TOXAV_FRIEND_CALL_STATE_FINISHED — нормальное завершение звонка. Данное состояние является последним состоянием для звонка (к этому моменту звонок завершен) и не комбинируется с другими состояниями.
  • TOXAV_FRIEND_CALL_STATE_SENDING_A — контакт начал передачу аудио потока.
  • TOXAV_FRIEND_CALL_STATE_SENDING_V — контакт начал передачу видео потока.
  • TOXAV_FRIEND_CALL_STATE_ACCEPTING_A — контакт начал прием аудио потока.
  • TOXAV_FRIEND_CALL_STATE_ACCEPTING_V — контакт начал прием видео потока

toxav_bit_rate_status_cb(friend_number, audio_bit_rate, video_bit_rate) — событие перегрузки сети, когда ядро не успевает отправлять данные с требуемым битрейтом. В качестве параметров ядро предлагает новые битрейты для аудио и видео потока. При чем сначала ядро пытается уменьшить битрейт видео потока, как занимающего основную полосу и только после отключения видео потока производится попытка уменьшения битрейта для аудио. Приложение может игнорировать данные рекомендации или использовать вызов toxav_bit_rate_set(friend_number, audio_bit_rate, video_bit_rate) для установки новых значений битрейтов. Битрейты задаются в Kb/s (килобиты в секунду), значение 0 сигнализирует об отключении соответствующего потока данных, значение -1 оставляет предыдущий установленный битрейт без изменений.

Для аудио кодека opus минимальный битрейт составляет 6, а значения выше 16-32 на мой слух уже не различимы для звука с веб-камеры.

Для видео кодека V8 я не смог найти рекомендуемых значений полосы пропускания в зависимости от размера кадра и FPS. В общем же случае для различных разрешений видео рекомендуются следующие базовые битрейты:
размер кадра, px битрейт, Kb/s
320x240 400
480x270 700
1024x576 1500
1280x720 2500
1920x1080 4000

toxav_audio_receive_frame_cb(friend_number, pcm, sample_count, channels, sampling_rate) — получение кадра аудио данных. В качестве параметров передается буфер данных, количество сэмплов, количество каналов и частота дискретизации (Hz). Для стерео звука 16-и битные отсчеты идут последовательно друг за другом для левого и правого канала. Размер буфера в байтах определяется как sample_count * channels * 2 byte, а длительность буфера в миллисекундах как sampling_rate / sample_count.

Поскольку PCM — это формат обычной импульсно-кодовой модуляции, то данные буфера можно передавать практически без преобразований в любой ЦАП или сохранять в файл WAV добавив соответствующий заголовок с описанием формата, однако следует быть готовым к тому, что в процессе разговора любые параметры аудио потока могут быть изменены вызывающей стороной.

toxav_video_receive_frame_cb(friend_number, width, height, y, u, v, ystride, ustride, vstride) — получение кадра видео данных в формате YUV420. Это оригинальный callback из ToxAV. Пример реализации преобразования из YUV420 в BGR можно найти, например, в исходном коде µTox (yuv420tobgr).

toxav_video_receive_frame_cb(friend_number, width, height, rgb) — получение кадра в формате RGB или BGR — это дополнительный callback обертки python, который отсутствует в ToxAV. Данный вариант используется для ускорения необходимых преобразований внутри библиотеки вместо их преобразований в pyhon (по факту используется yuv420tobgr из кода µTox). Формат RGB или BGR задается вызовом toxav_video_frame_format_set с параметром:

  • TOXAV_VIDEO_FRAME_FORMAT_BGR — формат BGR (используется в OpenCV).
  • TOXAV_VIDEO_FRAME_FORMAT_RGB — формат RGB.
  • TOXAV_VIDEO_FRAME_FORMAT_YUV420 — оригинальный callback из ToxAV с форматом YUV420.

Здесь и далее под RGB / BGR понимается 24-х битный формат без альфа-канала, где каждой из составляющих красного, зеленого и синего цвета отведено по 8 бит. Иногда этот формат именуется как RGB24 / BGR24 в зависимости от порядка следования цветовых составляющих красного и синего.

Так же как для аудио потока, формат видео потока может быть в любой момент изменен вызывающей стороной (например, изменится размер кадра).

Обработка событий


Для обработки событий ToxAV используется периодический вызов метода toxav_iterate (по аналогии с вызовом метода ядра tox_iterate) с рекомендуемым интервалом между вызовами равному значению, которое вернет вызов toxav_iteration_interval (по аналогии с вызовом ядра tox_iteration_interval).

Рабочий цикл для ToxAV рекомендуется обслуживать в отдельном потоке, т.к. в отсутствии аудио/видео вызовов значение toxav_iteration_interval будет составлять 200 ms и поток будет спать большую часть времени:

import threading

class EchoAVBot(ToxAV):
    def __init__(self, core):
        ...
        self.running = True
        self.iterate_thread = threading.Thread(target = self.iterate_cb)
        self.iterate_thread.start()

    def iterate_cb(self):
        while self.running:
            self.toxav_iterate()
            interval = self.toxav_iteration_interval()
            time.sleep(float(interval) / 1000.0)

    def stop(self):
        self.running = False
        self.iterate_thread.join()
        self.toxav_kill();

Поскольку для python у нас существует GIL (Global Interpreter Lock), то нам не следует заботиться о необходимости синхронизации между потоками, однако в других языках могут возникать «неожиданные» сюрпризы, когда toxav_*_cb события могут быть вызваны из потока обслуживающего tox_iterate вместо потока обслуживающего toxav_iterate. Таким событием, например, является событие завершения звонка toxav_call_state_cb — используйте соответствующие примитивы синхронизации с учетом возможности deadlock внутри ядра Tox.

Управление звонками


Для создания исходящего звонка используется метод toxav_call(friend_number, audio_bit_rate, video_bit_rate), где в качестве аргументов указывается идентификатор контакта из контакт-листа и исходящий аудио и видео битрейт (Kb/s). Значение битрейта 0 блокирует отправку соответствующего потока, который можно будет включить позже вызовом toxav_bit_rate_set.

У ToxAV отсутствует привычный нам контроль посылки вызова — не принятый исходящий вызов будет длиться пока вызов не будет отменен по какому-либо внутреннему таймауту самого приложения (приложение «положит трубку»), или контакт не уйдет в offline («обрыв сети» и событие toxav_call_state_cb с параметром TOXAV_FRIEND_CALL_STATE_FINISHED). По умолчанию прием аудио и видео данных от контакта разрешен и может быть изменен вызовом toxav_call_control.

Входящий вызов определяется событием toxav_call_cb, после чего вызов может быть как полностью проигнорирован, так и обработан ответом на звонок или изменением его состояния через toxav_call_control.

Для ответа на звонок используется вызов toxav_answer(friend_number, audio_bit_rate, video_bit_rate), где в качестве аргументов указывается идентификатор контакта из контакт-листа, совершающего входящий вызов, и исходящий аудио и видео битрейт (Kb/s). Значение битрейта 0 блокирует отправку соответствующего потока, который можно будет включить позже вызовом toxav_bit_rate_set.

Для контроля состояния звонка используется вызов toxav_call_control(friend_number, control), где в качестве управляющего параметра могут быть использованы следующие константы:

  • TOXAV_CALL_CONTROL_RESUME — возобновление звонка, который ранее был установлен на паузу, не может использоваться до ответа на звонок.
  • TOXAV_CALL_CONTROL_PAUSE — установка звонка на паузу (удержание вызова, hold), не может использоваться до ответа на звонок.
  • TOXAV_CALL_CONTROL_CANCEL — сброс входящего вызова или завершение активного звонка.
  • TOXAV_CALL_CONTROL_MUTE_AUDIO — отправка респонденту запроса на остановку передачи аудио данных (респондент может проигнорировать данный запрос).
  • TOXAV_CALL_CONTROL_UNMUTE_AUDIO — оправка респонденту запроса на возобновление передачи аудио данных.
  • TOXAV_CALL_CONTROL_HIDE_VIDEO — отправка респонденту запроса на остановку передачи видео данных (респондент может проигнорировать данный запрос).
  • TOXAV_CALL_CONTROL_SHOW_VIDEO — оправка респонденту запроса на возобновление передачи видео данных.

Передача аудио и видео потоков


Для передачи аудио потока используется вызов toxav_audio_send_frame(friend_number, pcm, sample_count, channels, sampling_rate), где в качестве аргументов указывается идентификатор контакта из контакт-листа, буфер PCM данных, количество сэмплов, количество каналов и частота дискретизации. По аналогии с toxav_audio_receive_frame_cb для стерео потока 16-и битные отсчеты для левого и правого канала должны идти последовательно друг за другом.

Следует напомнить, что максимальное количество каналов составляет 2 (стерео), частота дискретизации может принимать значения 8, 12, 16, 24 и 48 KHz и из за особенностей кодека opus, количество сэмплов должно соответствовать длительности кадра в 2.5, 5, 10, 20, 40 или 60 ms.

Для передачи кадра видео потока используется вызов toxav_video_send_frame, который в python обертке переименован в toxav_video_send_yuv420_frame(friend_number, width, height, y, u, v), где в качестве аргументов указывается идентификатор контакта из контакт-листа, ширина и высота кадра в пикселях, буферы Y (яркость) и цветоразностные U-V.

Пример реализации преобразования из BGR в YUV420 можно найти, например, в исходном коде µTox (bgrtoyuv420). В python обертке для удобства реализованы методы toxav_video_send_bgr_frame(friend_number, width, height, bgr) и toxav_video_send_rgb_frame(friend_number, width, height, rgb), которые осуществляют данные преобразования самостоятельно (по факту используется bgrtoyuv420 из кода µTox).

Примеры


Python является достаточно удобным языком для прототипирования и изучения принципов работы. Для наглядой демонстрации всего вышеизложенного я написал несколько примеров:

echobot.py — обычный текстовый echo-бот, который отвечает отправленным ему текстом. Дополнительно демонстрирует управление контактами, статусами, аватарками, файлами. Является базовым для реализации остальных примеров.

echoavbot.py — аудио и видео echo-бот, который передает обратно вызывающему отправленные ему аудио и видео потоки. Может быть удобен для изучения работы влияния сети на качество передачи данных, возникающие задержки, тестирования собственного оборудования.

avbot.py — бот для имитации респондента, передает аудио и видео данные с собственных устройств. Может быть удобен для наблюдения за домашними животными во время своего отсутствия :)

Заключение


Проект Tox жив и активно развивается. Разрыв отношений с Tox Foundation нисколько не затормозил развитие проекта и, кажется, дал ему второе дыхание — начали приниматься PR, разбираться тикеты, активно наполняется новый сайт и ведется работа над документацией. Надеюсь, в скором времени в ядро будет влита ветка работы с групповыми чатами и ядро примет законченный предрелизный вид.

Ссылки


  • tox.chat — официальный сайт проекта Tox.
  • Проект ядра toxcore на github.
  • Проект python-обертки pytoxcore на github.
  • Проект для сборки бинарных версий клиентов и библиотек tox.pkg на github включая oldstable дистрибутивы типа Debian Wheezy, Ubuntu Precise и CentOS 6, на которых «официальные» сборки клиентов могут не запускаться.

Комментарии (23)


  1. r44083
    13.12.2015 23:17
    +6

    Большое спасибо за статью. Уже больше года использую TOX, он меня только радует.


    1. monah_tuk
      14.12.2015 12:57
      +2

      Я вот только начинаю щупать, не подскажите, какие бест-практики для следующего кейса: есть комп дома, на работе и мобильный телефон и хочется со всех их общаться с одного ToxID. Или каждое устройство подразумевает только новый ToxID?


      1. BiTHacK
        14.12.2015 13:36
        +1

        Насколько мне известно, на данный момент такой кейс весьма проблематичен и лучше использовать несколько разных профайлов либо шарить один каким-то образом на все устройства и использовать его только с одно устройства в один момент времени. Обсуждение реализации этой возможности в ядре toxcore происходит тут: https://github.com/irungentoo/toxcore/issues/843


      1. r44083
        14.12.2015 20:35

        На данный момент решением будет: везде использовать один и тот же файл *.tox. В таком случае у вас везде будет один TOX ID, но как уже было сказано, в один момент времени в сети может быть только один активный клиент из нескольких клиентов с одинаковыми TOX ID (они будут перебивать друг друга и возможно конфликтовать).


  1. isotoxin
    14.12.2015 01:17
    +4

    Я разрабатываю windows клиент для этой сети: Isotoxin. По фичам я уже обогнал qTox.
    Буквально пару недель назад сделал поддержку видео. Работает сносно, до тех пор, пока ваш канал достаточно широк для видеопотока. Как только канала становится недостаточно, происходит потеря пакетов. В ядре есть два способа послать пакет с данными: т.н. lossless и lossy. В чем отличие? Отличие в том, что lossy пакет просто дропается, если ядро не смогло поставить его в очередь на отправку, а lossless будет отослан в любом случае. lossy пакеты используются для передачи видео и аудио. Казалось бы, все правильно. Нет, не правильно! Звук — еще куда ни шло — там все пакеты независимы. А вот с видео так делать нельзя. Точнее нельзя делать так, как это сделано в ядре tox. Получается, при таком подходе — когда часть пакетов, из которых состоит кадр, не долетает до адресата — на приемнике случается цифровой мусор.
    Тикет я им написал, но они его почему-то оставили без внимания. Я хочу сделать свою собственную реализацию передачи видеоданных, чтобы убедиться, что ход моих мыслей верен, а потом предложу эту реализацию ребятам из toxcore-team.


    1. liotcheg
      14.12.2015 02:13
      +2

      Да, клиент очень интересный! Правда, я так и не понял, как импортировать .tox профиль.
      Не хотите написать статью о клиенте?


      1. isotoxin
        14.12.2015 11:43
        +1

        Импортировать профиль можно при создании нового подключения к сети Tox. Там в диалоге есть поле для имени файла, из которого читать профиль.
        Что касается статьи, я думал над этим, но пока не придумал, как написать так, чтобы это было интересно аудитории хабра и не выглядело как реклама.


    1. Halt
      14.12.2015 08:00
      +1

      А можно ли часть видеопотока отправлять с гарантированной доставкой? Скажем, каждый N-й I-frame слать как lossless. Тогда при потере промежуточных фреймов у декодера все равно будет точка восстановления. Другое дело, что I фреймы гораздо больше чем P и B. Поэтому слать все подряд будет неэффективно.

      P.S.: Использовал терминологию MPEG2 вещания. Не знаю, как оно сделано в вашем кодеке.


      1. isotoxin
        14.12.2015 11:57
        +1

        Это нужно сделать. Вопрос только в том, кто это будет делать. Я не разработчик ядра, но тем не менее в своем клиенте я могу сделать другой способ передачи видео. Понятно, что он будет работать только при звонках на мой-же клиент. Когда я так сделаю, то у меня будет что предложить разработчикам ядра — реально рабочее.
        При общении с разработчиками ядра, меня несколько огорчило их отношение к предложениям извне их команды. Я еще могу понять нежелание их мейнтейнера ядра (irungentoo) убрать из кода C99 зависимости (я автор сборки под Visual Studio), но когда я им явно описал место в коде, где можно с'экономить на копировании несжатого видеокадра (а это несколько мегабайт в секунду), и даже исправил код в своем форке toxcore и убедился что фикс работает (сейчас Isotoxin на исправленом коде — все ok), тот факт, что это исправление до сих пор не вошло в основной репозиторий выше моего понимания.
        Так что улучшать toxcore я не перестану, но вот войдут ли эти улучшения в другие клиенты — это вопрос.


        1. antonbatenev
          14.12.2015 14:59

          А можно ссылку на PR с экономией на несжатом кадре? Может, себе утащу тоже.


    1. antonbatenev
      14.12.2015 10:07
      +1

      В теории, кодек VP8 допускает потерю части пакетов («кадры типа Recovery строятся не на базе непосредственно предшествующих кадров»). Плюс в ядре kf_max_dist установлено в значение 48 — т.е. максимум каждые 48 кадров будет отправляться полный опорный кадр (при частоте кадров в 25 FPS получается изображение восстановится за ~2 секунды максимум после восстановления пропускной способности канала).

      Если отсылать пакеты с гарантированной доставкой, то при недостатке полосы она просто забьется совсем. Кажется в этом случае лучшая стратегия — это адаптивно уменьшать битрейт для кодирования видео (следуя рекомендациям toxav_bit_rate_status_cb) или уменьшать на лету размер отсылаемого кадра.

      Что касается собственной реализации и экспериментами с ней, то я тут только могу поддержать.


      1. isotoxin
        14.12.2015 12:33

        > изображение восстановится за ~2 секунды максимум
        тут проблема в том, что опорные кадры тоже приходят битыми, т.к. часть пакетов, из которых состоит опорный кадр, не долетает до приемника.
        Моя идея состоит в том, чтобы:
        1. опорные кадры слать lossless
        2. остальные кадры слать lossy, но только первый пакет. Если первый пакет неключевого кадра отправлен, остальные пакеты этого кадра слать lossless

        Вобщем, идея в том, чтобы кадр либо целиком не дошел, либо целиком дошел. Иначе изображение портится, что сейчас и происходит


  1. Randl
    14.12.2015 02:19

    А что с offline-messaging? ИМХО, необходимая, хоть и труднореализуемая фича.


    1. Gordon01
      14.12.2015 04:55
      -4

      Можно сделать как в скайпе


    1. antonbatenev
      14.12.2015 10:11

      Кажется, что в ядре такой фитчи в обозримом будущем не будет и ее оставят на усмотрение клиентского приложения.


    1. BiTHacK
      14.12.2015 12:27

      Вот тут обсуждение этой возможности: https://github.com/irungentoo/toxcore/issues/870


    1. monah_tuk
      14.12.2015 13:00

      Ещё неплохо, когда в клиенты более-менее прозрачно интегрируют поддержку существующих ToxDNS серверов. Но ваш вопрос тоже животрепещущий.


      1. liotcheg
        15.12.2015 01:45

        А в каких клиентах она не внедрена? Пробовал в qTox, uTox, antox, isotoxin, toxy. Везде работает.


        1. monah_tuk
          15.12.2015 03:31

          Поддержка да, я про создание алиаса (регистрацию имени). В qTox не увидел.


  1. 0xd34df00d
    15.12.2015 04:28

    Я тоже занимаюсь разработкой Tox-клиента (модуль для этих самых личкрафтов, Azoth Sarin). Как раз на днях занимался добавлением аудиозвонков. С эхоботом удаётся созвониться и даже некоторое время меня слышно, но потом звук пропадает. В чём проблема, выяснить пока не удалось.

    Не сталкивались ли вы с таким?


    1. antonbatenev
      15.12.2015 04:50

      Проблема возникает только в личкрафте или в стандартном клиенте тоже? Если первое, то возможная причина deadlock потоков в ядре Tox. Если второе, то только gdb в руки и смотреть чем оба занимаются в это время. Я тестировал с qTox на интервале в несколько часов, но уверен, что тестирование покрыло далеко не все возможные ситуации.


      1. 0xd34df00d
        15.12.2015 04:54

        Судя по отладочным логам, ядро вполне себе живо, какие-то фреймы отправляет и какие-то фреймы принимает, непохоже на дедлок.

        А вы запускали два инстанса, один — qTox, другой — ваш, и общали их друг с другом? Если да, то я-то общался с предположительно заведомо работающей штуковиной, поэтому не уверен, что проверка стандартного клиента что-то даст (если это, конечно, мне не «повезло» с ревизией библиотеки). Но попробую, да, спасибо.


        1. antonbatenev
          15.12.2015 05:19

          Сейчас попробовал с qTox в течении порядка 7 минут — вполне рабочая штука как для аудио так и для видео, т.е. проблем не наблюдаю с оф-клиентом.