Асинхронные голосовые уведомления

Сообщение notify позволяет облаку проиграть одностороннее голосовое уведомление, пока устройство простаивает. Оно не открывает аудиоканал диалога и никогда не включает передачу звука с микрофона. Чтобы начать диалог, по-прежнему нужно явное действие пользователя по пробуждению.

Формат сообщения

Облако отправляет следующий JSON через текущее управляющее соединение протокола устройства. Для устройства на MQTT это существующий управляющий топик MQTT:

{
  "type": "notify",
  "audio_url": "https://cdn.example.com/audio/task-complete.ogg",
  "subtitles": [
    {
      "start_ms": 0,
      "text": "Your task is complete."
    },
    {
      "start_ms": 1800,
      "text": "The result has been saved."
    }
  ]
}

Поле audio_url обязательно. Принимаются URL как с http://, так и с https://. HTTP удобен при разработке в локальной сети, а в production можно требовать HTTPS при формировании URL.

Поле subtitles необязательно. Каждый элемент содержит время начала в миллисекундах по шкале медиа и текст для показа. Устройство сортирует элементы по start_ms и обновляет экран только тогда, когда воспроизведение переходит к новому элементу субтитров.

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

Аудиоответ

По audio_url должен возвращаться успешный ответ 2xx с одним потоком моно Ogg Opus. Поддерживаются ответы как с Content-Length, так и с chunked-кодированием передачи. Устройство не следует за перенаправлениями.

Файл читается постепенно. Устройство не выделяет память под полную длину ответа и не скачивает файл целиком до начала воспроизведения. При потоковом воспроизведении уведомления используются уже существующая ограниченная очередь HTTP-ответа, буфер логических пакетов Ogg на 2 КБ, общая очередь декодирования Opus на 20 пакетов и существующая очередь воспроизведения PCM на два кадра. Очередь декодирования соответствует 1,2 секунды при обычной длительности пакета прошивки 60 мс. Эти буферы создают обратное давление TCP (backpressure), благодаря чему функция остаётся работоспособной на устройствах без PSRAM.

На устройствах с отдельным WakeNet из LiteAudioEngine ресурсы WakeNet освобождаются на время воспроизведения уведомления и создаются заново, когда устройство возвращается в Idle. Устройства на AFE сохраняют прежнее поведение локального пробуждения.

Длительность пакета Opus читается из байта TOC Opus, а не передаётся в сообщении MQTT. Принимаются целые длительности пакетов от 5 до 120 мс, которые поддерживает декодер прошивки. Текущая реализация отклоняет стереопотоки, обнаруженные структурные ошибки Ogg или Opus, неполные потоки, слишком большие логические пакеты и пакеты по 2,5 мс.

Поведение устройства

Устройство принимает notify только в состоянии Idle. После этого оно выполняет такие действия:

  1. Переходит во внутреннее состояние Notifying и переключает плату в режим производительности.
  2. Отключает обычную обработку голоса и передачу звука с микрофона.
  3. Очищает предыдущее воспроизведение и ставит в очередь встроенный звук всплывающего окна.
  4. Запускает один HTTP GET в фоновой задаче.
  5. Постепенно демультиплексирует пакеты Ogg и отправляет их прямо в существующую очередь декодирования Opus.
  6. Показывает субтитры в соответствии с позицией медиа пакетов Opus, доходящих до задачи аудиовывода.
  7. Возвращается в Idle только после успешного завершения HTTP-потока и воспроизведения всего звука из очередей.

Звук всплывающего окна ставится в очередь до запуска HTTP-задачи, поэтому удалённое аудио не может прозвучать раньше него. Установление HTTP-соединения всё равно идёт одновременно с фактическим воспроизведением этого звука.

Если после начала воспроизведения удалённого аудио, но до завершения HTTP-потока все очереди воспроизведения опустели, устройство пишет в журнал Notification playback underrun #<count> at <position> ms. Переход от звука всплывающего окна к потоку и обычное окончание воспроизведения underrun не считаются.

Устройство не открывает аудиоканал UDP, не отправляет start-listening и не переходит в Listening автоматически. Устройства на AFE могут продолжать локальное обнаружение слова пробуждения во время воспроизведения. Устройства без эхоподавления при воспроизведении сохраняют прежнее поведение пробуждения в режиме речи; аппаратные средства пробуждения по-прежнему могут отменить уведомление.

Действие пробуждения отменяет HTTP-производителя, очищает поставленное в очередь аудио уведомления и продолжает обычный порядок пробуждения. Потеря сети, ошибки HTTP и недопустимое аудио также отменяют воспроизведение и возвращают устройство в Idle. Второе уведомление, полученное, пока устройство занято, игнорируется.

Пересылка через xz-mqtt

xz-mqtt пересылает сообщение существующим методом Redis RPC:

{
  "method": "forward",
  "clientId": "device-client-id",
  "params": {
    "type": "notify",
    "audio_url": "https://cdn.example.com/audio/task-complete.ogg",
    "subtitles": [
      {
        "start_ms": 0,
        "text": "Your task is complete."
      }
    ]
  }
}

Результат RPC содержит булево значение, возвращённое операцией отправки MQTT:

{
  "success": true
}

Этот результат означает, что xz-mqtt записал сообщение публикации в текущее соединение устройства, находящегося в сети. Он не подтверждает ни получение, ни воспроизведение. xz-mqtt не создаёт сессию UDP, не запускает мост чата, не проксирует файл Ogg и не хранит уведомления для устройств, находящихся вне сети.

Источник: docs/notify.md в 78/xiaozhi-esp32