Мой блог
Как транслировать одно окно Windows на планшет через браузер без лишнего софта
Мне потребовалось организовать трансляцию интерфейса конкретной программы с рабочего ПК на планшет, причем исключительно в режиме просмотра. Готовых утилит и протоколов на рынке предостаточно, но большинство из них превращают мобильное устройство во второй монитор, требуют установки громоздких клиентских приложений или транслируют дисплей целиком. Я решил разработать собственную компактную утилиту WindowStream, которая решает эту узкую задачу за пару кликов.
Подробности реализации и исходный код проекта доступны в официальном репозитории на GitHub, где каждый желающий может изучить архитектуру.
Что представляет собой утилита WindowStream
Эта программа создана на языке Python специально для операционной системы Windows и функционирует прямо из системного трея. Пользователь просто открывает контекстное меню, выбирает целевое приложение, после чего запускается вещание по локальной сети в формате MJPEG поверх стандартного протокола HTTP. На самом планшете, смартфоне или гостевом компьютере не нужно устанавливать дополнительный софт — достаточно запустить любой современный браузер и ввести адрес вида http://192.168.1.20:8080/.
В ходе работы я тестировал утилиту на Windows 11, транслируя картинку на Android-планшет, а также на смартфон, соединенный с ПК через точку доступа Wi-Fi. На стороне мобильного браузера интерфейс поддерживает жесты масштабирования (зум до восьми крат), панорамирование, двойной тап для сброса, полноэкранный режим, а также специальный индикатор статуса соединения (активный стрим, пауза или обрыв связи). При перезапуске сервера или временной потере сети страница автоматически восстанавливает подключение, а заголовок вкладки динамически подстраивается под имя транслируемого окна.
Архитектура и внутреннее устройство программы
Чтобы добиться высокой отзывчивости и минимальных задержек, я заложил в проект несколько проверенных механизмов захвата, оптимизации трафика и сетевой раздачи.
Механизмы захвата изображения
В коде предусмотрено два принципиально разных режима захвата. Первый называется screen: в этом случае через библиотеку mss копируется прямоугольная область экрана, где располагается целевое окно. Этот метод работает невероятно быстро и универсально, однако если поверх вещаемого приложения случайно окажется другое окно, оно неизбежно попадет в итоговый кадр.
Второй режим называется printwindow. Здесь задействуется стандартная функция Win32 API PrintWindow с флагами PW_CLIENTONLY и PW_RENDERFULLCONTENT. Приложение самостоятельно прорисовывает свой интерфейс в буфер, поэтому перекрывающие окна ему не страшны. Главный нюанс заключается в том, что некоторые современные программы с аппаратным ускорением рендеринга выдают в таком режиме сплошной черный экран. Именно поэтому я вынес переключатель режимов в настройки, установив printwindow в качестве стандартного значения по умолчанию.
Захват всегда ведется строго по клиентской области целевого приложения, исключая системную рамку и заголовок, причем в нативных пикселях. Программа изначально объявлена как DPI-aware, иначе при масштабировании интерфейса Windows на 125 или 150 процентов координаты захвата начинают сильно смещаться.
Оптимизация трафика и кодирование
Передавать статичную картинку по сети бессмысленно, поэтому я добавил проверку изменений. Для каждого захваченного кадра вычисляется контрольная сумма CRC32 от сырого буфера памяти. Если текущая хэш-сумма совпадает с предыдущей, кадр просто отбрасывается. Для статических программ и интерфейсов это решение радикально снижает нагрузку как на центральный процессор, так и на локальную сеть.
Сжатие в формат JPEG выполняется с помощью библиотеки PyTurboJPEG, которая выступает быстрой оберткой над libjpeg-turbo. Если масштаб выставлен на единицу, сырой буфер BGRX от mss передается в кодировщик напрямую, минуя промежуточные преобразования через библиотеку Pillow. Если же libjpeg-turbo в системе не обнаружен, утилита выдает предупреждение и автоматически переключается на Pillow — работает чуть медленнее, но сохраняет полную работоспособность.
Сетевая раздача и веб-сервер
Я намеренно отказался от тяжелых веб-фреймворков в пользу стандартного класса ThreadingHTTPServer и потокового метода multipart/x-mixed-replace. Один фоновый поток постоянно занят захватом и кодированием, а готовый JPEG записывается в общий буфер вместе с уникальным порядковым номером и таймстампом. Доступ к буферу защищен примитивом синхронизации Condition.
Каждое новое клиентское подключение обслуживается в персональном потоке: он ожидает поступления нового номера кадра и сразу отправляет свежие данные зрителю. Если конкретный планшет начинает тормозить, его очередь кадров не раздувается, а старые данные просто пропускаются, не создавая проблем для остальных подключений.
Если содержимое окна не меняется в течение пяти секунд, сервер принудительно пересылает последний актуальный кадр повторно, но с тем же номером. Это позволяет поддерживать соединение активным и помогает браузеру четко разделять ситуации «картинка не обновилась» и «полностью пропала связь». Обычный тег <img src=”/stream”> успешно отображает этот поток, игнорируя лишние служебные HTTP-заголовки.
Клиентская часть в браузере
Потоковые данные на стороне страницы разбираются вручную через JavaScript-интерфейс fetch. Мне потребовался такой подход ради кастомных заголовков X-Seq и X-Ts, по которым вычисляется реальная кадровая частота и сетевая задержка. Для оценки лага клиент каждую секунду опрашивает служебный эндпоинт /status со временем ПК, после чего по серии быстрых запросов вычисляет примерное расхождение системных часов.
С какими техническими трудностями я столкнулся
Разработка выявила несколько неочевидных нюансов, о которых разработчики обычно не пишут в открытой документации популярных библиотек.
Проблемы с динамическим треем
Библиотека pystray в среде Windows формирует системное меню всего один раз при старте. Из-за этого список активных окон быстро устаревал: приложения, запущенные после старта утилиты, просто не попадали в меню. Проблему удалось решить через переопределение класса pystray.Icon с перехватом события правого клика мыши (WM_RBUTTONUP) и принудительным вызовом метода update_menu() перед отрисовкой списка.
Особенности вывода справок из собранного EXE
Я собираю исполняемый файл через PyInstaller с ключами –onefile и –noconsole, чтобы приложение не раздражало пользователя черным окном терминала. Обратной стороной такого подхода стал полный отпуск потока stdout: ключи вроде –help или –list физически ничего не выводили в консоль. Простые попытки вызова AttachConsole(ATTACH_PARENT_PROCESS) не срабатывали, так как упаковщик запускает процесс через собственный безконсольный загрузчик.
Пришлось программно подниматься по дереву родительских процессов в поисках активной консоли. Кроме того, поскольку приложение считается оконным, приглашение командной строки возвращает управление мгновенно, поэтому для корректного чтения справки требуется запускать утилиту через конструкцию start /wait “” WindowStream.exe –help. Подключение к консоли задействуется исключительно для этих сервисных ключей, иначе закрытие терминала автоматически привело бы к завершению работы самого трей-приложения.
Надежный поиск окон и юнит-тестирование
Заголовки многих программ динамически меняются в зависимости от открытого файла, поэтому жесткая привязка по точному совпадению строки быстро ломалась. Я реализовал дополнительную логику: вместе с текстом заголовка сохраняется точное имя исполняемого процесса. Если точный заголовок потерян, утилита находит самое крупное видимое окно этого же процесса.
Всю логику конфигураций, локализации и интерфейса я покрыл юнит-тестами, которые успешно выполняются даже на Linux благодаря подмене системных модулей pywin32, winreg, mss и pystray специальными заглушками. Такой подход оправдал себя незамедлительно: тесты выловили реальный баг, когда некорректное строковое значение в файле settings.json вызывало падение из-за конфликта аргументов в переводчике.
Инструкция по использованию программы
Самый быстрый способ начать работу — скачать готовый бинарник WindowStream.exe из раздела релизов репозитория. Наличие интерпретатора Python на ПК не требуется. Поскольку файл не имеет цифровой подписи, защитник Windows SmartScreen и сторонние антивирусы могут выдать предупреждение — это стандартная ситуация для автономных сборок PyInstaller. При первом старте брандмауэр запросит доступ: обязательно разрешайте подключения только для частных локальных сетей.
После запуска в системном трее появится серый значок. По клику правой кнопкой мыши открывается список доступных окон, выбор которого мгновенно активирует трансляцию. Наведя курсор на иконку, можно увидеть подсказку с сетевым адресом для планшета, текущей частотой захвата, скоростью новых кадров и общим числом подключенных зрителей.
Для развертывания из исходного кода выполните стандартные команды:
- git clone https://github.com/eugenyh/WindowStream.git
- cd WindowStream
- pip install -r requirements.txt
- python window_stream.py
Рекомендуется отдельно установить пакет libjpeg-turbo для максимальной производительности кодирования. Все пользовательские параметры сохраняются в файле %APPDATA%\WindowStream\settings.json, причем аргументы командной строки всегда имеют приоритет, а ключ –save позволяет зафиксировать текущие настройки на диск. Реализована функция автозапуска вместе с Windows: если целевое приложение еще не запустилось к моменту входа пользователя в систему, утилита будет терпеливо опрашивать систему каждые пять секунд. Интерфейс переведен на русский и английский языки.
Главные ограничения утилиты
Честный обзор невозможен без упоминания слабых мест разработанного инструмента. В программе полностью отсутствуют механизмы шифрования и авторизации, используется незащищенный протокол HTTP. Любой участник вашей локальной сети, узнавший IP-адрес и порт, сможет просматривать транслируемое окно. По этой причине утилиту стоит использовать исключительно в доверенных домашних или офисных сетях, избегать стриминга конфиденциальных данных в публичных местах и никогда не пробрасывать порты наружу в интернет.
Существуют и технологические ограничения захвата: свернутое приложение не передает кадры, оставляя на планшете застывшую картинку. Участки окна, выходящие за физические границы экрана, операционной системой не перерисовываются. В режиме screen захватываются любые перекрывающие объекты, а в режиме printwindow некоторые программы выдают черный экран. Если целевой софт запущен с правами администратора, сама утилита WindowStream также потребует повышения привилегий. Звуковая дорожка и элементы дистанционного управления не поддерживаются — это чисто пассивный инструмент визуального контроля.
Разработанный проект решает конкретную локальную проблему, умещая всю необходимую сетевую и графическую механику в единственный легко читаемый файл. Программа распространяется под свободной лицензией MIT, поэтому вы можете смело дорабатывать ее под свои сценарии.
