В этой статье мы разберем несколько существующих методов записи экрана и способы трансляции по сети с минимальной задержкой.

Предыстория

Разрабатывая продукт для саппорта появилась потребность ускорить удаленное подключение к пользователю для решения локальных проблем с устройством, для этих целей использовался TeamViewer или AnyDesk и все было хорошо кроме стадии когда нужно скачать клиент, сообщить логин и пароль либо уникальный номер

Было решено ускорить это т.к. не все готовы дать удаленный доступ и были те кто готов но возникали сложности запустить клиент, можно было потратить 10 минут чтобы запустить и 2 минуты чтобы решить вопрос

Лучшим решением стала демонстрация экрана в браузере, пользователь получает уникальную ссылку и запускает демонстрацию экрана, управление хоть и не доступно но этот процесс стал настолько быстрым что и этого было достаточно и больше людей готовы показывать но не скачивать что-то что они не понимают и давать доступы к устройству, связь поддерживалась обычно через телефонный звонок из браузера оператора техподдержки

С момента как я реализовал этот метод и погрузился в мир WebRTC и видеостриминг я увлекся настолько, что решил узнать на сколько возможно реализовать проект использования какого-то девайса где есть браузер в качестве расширенного монитора для основного компьютера который не уступит по задержке (ну или хотя бы будет иметь не критичную) физическому монитору который подключен в отдельный порт

Скрытый текст

Это относительно возможно но карта захвата справится с этим лучше, правда судя по ценам хорошая стоит как второй монитор а то и дороже

Исследование

Мой путь начался с поиска решений программного захвата экрана для дальнейшей трансляции

Первым инструментом стал ffmpeg и протоколы rtsp/rtp. В качестве клиента который будет забирать rtsp и проксировать в WebRTC я выбрал проект libdatachanne. Знаний на тот момент в C/C++/C#/Go были недостаточными чтобы написать свой с нуля

Для захвата экрана у ffmpeg есть параметры:

  • -f gdigrab

  • -i desktop

для кодека использовался H264

Все возможные комбинации описывать не буду, в крации более менее реал тайма получалось достичь только если скалировать минимально видео, в высоком разрешение 1920x1080 60 кадров даже не запустилось с параметром -tune zerolatency, без него есть задержка секунды 4

Почему не использовать VLC?

Проигрывание RTSP в VLC мною использовалось только как тест того что вообще работает трансляция, у него тоже есть своя задержка и возможно в настройках её можно убрать, я не нашел где именно.

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

Следующим решением стало написание своего захвата экрана т.к. я грешил на то что ffmpeg тоже с задержкой грабит экран и ничего с этим не сделать (на деле они используют тот же DirectX просто я не знал этого)

Я начал искать как получать кадры экрана Windows для дальнейшей обработки и посылки по сети, среди опенсорса натыкался на решения с использованием GetDC

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

Дальнейшие поиски сводились к сложным методам связанных с DirectX или Nvidia с которыми в то время так же не хватало навыков разобраться

Display Duplicate Api

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

Как оказалось они используют DirectX DXGI и собственно в самом окне при выборе метода захвата они так же это указывают

Осталось почитать документацию по Desktop Duplicate Api и попробовать реализовать этот способ думал я, но когда было мало опыта в С/C++ и нет опыта с DirectX стало ясно что это будет сложно и надолго

Благо есть опенсорс и я нашел библиотеку которая компилирует С++ приложение с апи для Node js node-win-desktop-duplication и это сразу дало буст на то чтобы разобраться как оно устроено и на основе неё написать свое решение.

Чтобы убедиться что написанный код работает наравне с захватом в OBS я написал десктопное приложение с окном в центре которого отрисовываются полученные фреймы, удавалось забрать 60-90 фреймов в секунду, результат был идеальным но возможно немного совсем малость уступал OBS в плавности смены кадров, это особенно заметно на мониторе 144 Гц, думаю связано с тем, что OBS скалирует фреймы т.к. видно что качестве немного хуже чем в моем решении но даже с этим можно было приступать с созданию решения по трансляции в локальной сети

Скриншот приложения которое дублирует экран
Скриншот приложения которое дублирует экран


У меня уже был опыт работы с Pion на Go и в целом на нём код писать легче. Я начал изучать CGO, задумка была в том чтобы собрать приложение Go которое забирает фреймы из скомпилированной библиотеки на C++

Сначала я попробовал отправлять фреймы в вебсокет как бинарные данные и практически было что-то близкое к реал тайм но есть все же задержка из-за ограчений локальной сети, мой Ethernet имеет пропускную способность 1 Гигабит/с. Каждый фрейм весит 8 мегабайт, от 60 фреймов мы можем получать от библиотеки. Посчитаем какая пропускная способность должна быть

(8 * 60) * 8 = 3 840

8 мегабайт на 60 фреймов и конвертируем мегабайты в мегабиты умножив на 8

Получаем 3 840 Мегабит/c это почти 4 Гигабита в секунду, будь мой интерфейс 10 Гигабит/с то по вебсокету в теории не было бы задержки и я бы в реальном времени получал кадры и отрисовывал в браузере но только локально т.к. остальные девайсы не потянут такой поток данных и будет задержка.

Сжатие

Первым делом попробовал сжатие фреймов. Добавил в проект libpng но запаковка каждого фрейма в png отнимало время из-за чего снова задержка и хуже чем если отправлять бинарные исходники. Следующий вариант стал JPEG, уже быстрее происходила запаковка и фреймы сбросили вес до 400 килобайт но все еще задержка из-за чего этот вариант тоже не подходит проекту.

Стало понятно что если использовать WebSocket то быстрее всего работает передача исходников без запаковки но опять только локально на том же устройстве, если пробовать передавать в локальной сети ноутбуку по wifi то ситуация еще хуже даже с частотой 5G.

WebRTC и H264

Я приступил к варианту использования WebRTC т.к. у него есть преимущества в виде набора готовых протоколов таких как RTP, встроенных кодеков и используется UDP соединение в отличии от WebSocket который поверх TCP работает, в локальной сети конечно разницы между TCP UDP практически не заметишь но все же наличие кодеков это основное преимущество из-за которого нужно пробовать реализовать.

Я подключил в проект библиотеку x264-go чтобы фреймы передавать в него и далее направлять видеотреку.

Это стало прорывом, я увидел плавную картинку экрана в хорошем качестве и почти без задержки, но плавность немного как будто бы не хватало и еще заметил, что мое приложение начало активно использовать процессор, немного поработал над оптимизацией и нагрузка стала меньше, но все еще как будто бы не идеально, нет эффекта что это монитор который подключили в другой порт и мышь движется так же как на основном мониторе.

Gstreamer RTP over UDP

Я решил вернуться к приложению gstreamer т.к. хотелось выяснить как с ним работать и получу ли я необходимый результат, разобравшись с командой для захвата экрана узнал, что там используется все тот же DirectX.

gst-launch-1.0 d3d11screencapturesrc show-cursor=true ! 
video/x-raw,framerate=60/1 ! videoconvert ! 
x264enc tune=zerolatency bitrate=70000 speed-preset=ultrafast key-int-max=1 ! 
rtph264pay config-interval=1 ! 
udpsink host=127.0.0.1 port=5000 sync=false async=false

Транслировать мы будем RTP по протоколу UDP. В примерах Pion есть код который читает UDP трафик на указанном хосте и порту и получаемый буфер rtp пакетов шлет в трек

Результат стал немного лучше, картинка стала плавнее но задержка в 200-300 мс все же есть, все еще нет того же эффекта как если смотреть в OBS.

Попробовал поиграть с ресайзом учитывая что и OBS что-то делает с картинкой и видимо поэтому в окне она отображается с минимальной задержкой

gst-launch-1.0 d3d11screencapturesrc show-cursor=true ! 
video/x-raw,framerate=60/1 ! videoscale ! 
video/x-raw,width=960,height=540 ! 
videoconvert ! 
x264enc tune=zerolatency bitrate=70000 speed-preset=ultrafast key-int-max=30 ! 
rtph264pay config-interval=1 ! 
udpsink host=127.0.0.1 port=5000 sync=false async=false

Результат стал гораздо лучше, уже начал ощущать что задержки почти нет но все еще не дотягивает до OBS но почти не критично

Готовые решения

Одним из решений является Spacedesk. Задержка неплохая, однако по непонятым причинам со временем можно заметить фризы и артефакты изображения.
На той же сети решение Gstreamer RTP over UDP + WebRTC у меня работало стабильнее

Еще я пробовал Chrome Remote Desktop, стабильно работает но задержка около 400мс, решение выше так же оказалось эффективнее

Итог

Я сделал для себя вывод, что возможность передавать по сети изображение с монитора в реальном времени возможно если реализовать на C/C++ как серверную так и клиентскую часть с простым окном в котором выводится поток.
Еще думаю, что аппаратное решение в виде хорошей карты захвата, которое может ресайзить и кодировать, так же позволило бы максимально приблизиться к реальному времени.

WebRTC упрощает тем, что клиентскую часть можно написать парой строк кода и имеет поддержку на всех устройствах, но тем самым добавляет задержку из-за шифрования трафика, буфера которым нельзя управлять на клиенте и множеством протоколов которые можно было бы не использовать учитывая что проект для локальной сети.

Если я что-то упустил или где-то не прав, буду очень рад обсудить в комментариях.
Спасибо!

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


  1. NickDoom
    15.01.2025 07:51

    Ну то есть потребность никуда не делась, нет организованности в написании… Жаль.

    Да, и в «Оглоеде» ещё возможности GPU планшета можно будет использовать. Крошечные, но ведь при этом основная видеокарта разгрузится от, скажем, физики или ещё чего-то автономного, не связанного с рендерингом…


    1. Rahid Автор
      15.01.2025 07:51

      С решением на OpenGL были мысли, но когда получилось через DXGI забрать достаточное количество фреймов, то этого хватало чтобы перейти к следующему этапу и одновременно проблеме передачи по локальной сети

      В любом случае спасибо что поделились, многим будет полезно изучить разные способы)


  1. RISA
    15.01.2025 07:51

    Ещё стоит учитывать что TeamViewer и AnyDesk и др. не шлют полный кадр в единицу времени, а они отслеживают события системы и изменения картинки на рабочем столе и захватывают только изменённую область экрана.

    PS. Ещё есть интересная программа как замена AnyDesk в копилку программ и решений https://rustdesk.com/


    1. Rahid Автор
      15.01.2025 07:51

      Похоже на концепцию работы кодеков например h264 :)
      Он шлет ключевой фрейм (keyframe) и далее интерфреймы в которых только изменения от ключевого


      1. RISA
        15.01.2025 07:51

        Да, но это уже оптимизация на уровне кодека. Если обновления экрана вообще не было, то вообще не стоит делать этот кадр, или если обновляется область, то размер картинки уже меньше и для работы с ней нужно меньше ресурсов