Решил с понедельника открыть сезон стримов на Ютубе. Идея банальная: показывать вживую, как я проектирую и вайбкодю пет-проект. Ну как пет-проект… В мае ему уже исполнится год и по архитектуре и функциям он разросся настолько, что уже приходится относиться к нему со всем уважением :-)
Пошёл в ChatGPT, поделился идеей. «Идея замечательная!» — сказал чат и начал уже было расхваливать меня, но я его остановил. «Мне нужна помощь с OBS: хочу сделать в стриме плашку с информацией: кто я, что прямо сейчас делаю, ссылки, время, вот это всё». «Спокойствие, сейчас всё объясню!» — сказал ChatGPT — и именно с этого началась моя история.
OBS — это программа, популярная среди стримеров. Она позволяет захватывать видео с экрана, вебку, управлять всем этим делом с помощью сцен и транслировать результат, куда прикажете. И, главное, она бесплатная.
Я описал ЧатуГПТ свой сетап:
Три монитора и вебкамера;
На левом и центральном мониторе я буду работать и стримить;
На правом у меня будет висеть OBS и всё, что я не хочу показывать (Телеграм, файлы с заметками, дополнительное окно браузера и т.п.).
И объяснил, какие мне нужны сцены:
Левый монитор плюс моя вебка в правом нижнем углу;
Центральный монитор плюс моя вебка в правом нижнем углу;
Вебка на весь экран — когда я хочу отвлечься от работы и поделиться какой-нибудь офигительной историей.

«И вот для каждой из этих сцен мне нужна сверху плашка, на которой будут…»
Имя стримера (моё имя, Егор Камелев);
Информация о том, какой сейчас транслируется монитор (или вебка);
Редактируемая текстовая информация о том, что именно происходит на стриме прямо сейчас;
Ссылки на продвигаемые ресурсы;
Дата и время (чтобы можно было понять — это живой стрим или запись).
Кстати, изначально ЧатГПТ сам мне предложил содержимое плашки, но оно не было похоже на то, которое я только что описал выше. Он советовал показывать статус стрима «Live» и его название. Мне эта идея не приглянулась. На мой вопрос «Зачем показывать статус стрима, если его потом всё равно многие будут смотреть в записи?» он не нашёлся что ответить.
В общем, с горем пополам определились с содержимым плашки — и чат начал выдавать мне инструкцию.
Сначала сделай чёрную полупрозрачную подложку (а должен заметить, что я пользуюсь Виндой и решил спрятать нижний бар с пуском, поиском и прочими иконками; высота бара составляет 48 пикселей, и именно такую высоту я и задал для плашки; естественно, полупрозрачность там была абсолютно не нужна, т.к. я обрезал транслируемый экран на эту высоту);
Теперь добавь текстовый блок с твоим именем. Настрой его. Молодец;
Теперь добавь текстовый блок с указанием источника стрима.
В этот момент я прерываю чат. Я бы хотел сделать вертикальные разделители между элементами. Куда мне их лучше засунуть? В качестве отдельных элементов или как часть каждого следующего элемента? Чат посоветовал второе.
Теперь давай добавим текст про тему стрима. В качестве источника возьмём текстовый файл на твоём компе. Ты сможешь редактировать его и как только сохранишь — текст обновится.
«О, это круто», — подумал я. Но тут увидел, что вертикальные разделители как-то странно висят в воздухе относительно текстов и мне это не понравилось. Поэтому пришлось для каждого разделителя добавлять отдельный элемент сцены.
Теперь добавим блок со ссылками. Молодец
А теперь часики. Для того, чтобы отображать реальное время мы можем пойти двумя путями:
Первый: используем текстовый файл, в котором ты будешь периодически менять время вручную (в этом месте инструкции нужно было видеть моё лицо);
Второй: используем такой источник данных как «Браузер» и сделаем html-страничку и небольшой js, который будет автоматически обновлять время.
И я такой в этом моменте: «Чегоооо?».
После этого я спросил: «Я правильно понимаю, что мы можем сверстать любой html, использовать свой css и js — и транслировать это в стрим, даже не открывая браузера?». «Да» — ответил ChatGPT.
И в этот момент я понял, что вся моя предыдущая работа с кучей элементов со своими настройками — полностью перечёркивается.
— А почему же ты мне тогда не предложил сразу сверстать плашку целиком?
— Я не знал, сможешь ли ты разобраться с вёрсткой — и предложил самый простой и быстрый вариант для новичка.
— А зачем мне разбираться с вёрсткой, если ты сам отлично всё можешь верстать, и от тебя потребовалась бы лишь инструкция о том, как мне прикрутить это дело к OBS?
— Да, конечно, ты совершенно прав. Так было бы и правильнее, и быстрее!
Пожалуй, это был один из немногих моментов, когда я испытал какие-то эмоции в адрес чатаГПТ.
Через пятнадцать минут всё было готово. Я с помощью чата сверстал три html-ки (для двух мониторов и вебки), связал их с текстовым файлом, в котором мог редактировать тему стрима на лету. Настройки стилей вынес в отдельный файлик. И также создал отдельный файлик для всех скриптов. Проблема висящих разделителей исчезла (как и любая другая проблема, связанная с оформлением). Десять элементов в источниках сцены превратились в один.
Скрипты отвечали за обновление текста, за работу часиков, а также за то, чтобы особенно длинный текст превращался в бегущую строку.
Поделюсь с вами кодом.
Вот overlay для центрального монитора (оверлеи для других сцен — по аналогии):

<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="overlay.css"> </head> <body data-monitor="center"> <div class="header"> <div class="left"> <span class="name">Егор Камелев</span> <span class="divider">|</span> <span class="monitor"> <span id="monitor"></span> </span> <span class="divider">|</span> <span class="comment" id="comment"> <span class="comment-track" id="comment-track"> <span class="comment-text" id="comment-text-1"></span> <span class="comment-gap"></span> <span class="comment-text clone" id="comment-text-2"></span> </span> </span> </div> <div class="right"> <span class="link">t.me/normfreelancer</span> <span class="divider">|</span> <span class="link">normcrm.ru</span> <span class="divider">|</span> <span id="clock"></span> </div> </div> <script src="overlay.js"></script> </body> </html>
Вот css (каскадная таблица стилей):
html, body { margin: 0; padding: 0; width: 1920px; height: 48px; overflow: hidden; } * { box-sizing: border-box; } body { font-family: Arial, sans-serif; font-size: 22px; line-height: 1; color: #ffffff; } .header { width: 100%; height: 100%; display: flex; align-items: center; justify-content: space-between; padding: 0 18px; background: linear-gradient( 180deg, rgba(20,20,22,0.96), rgba(10,10,12,0.96) ); overflow: hidden; } .left { display: flex; align-items: center; gap: 14px; min-width: 0; flex: 1 1 auto; white-space: nowrap; overflow: hidden; } .right { display: flex; align-items: center; gap: 14px; flex: 0 0 auto; margin-left: 24px; white-space: nowrap; } .name { font-weight: 600; flex: 0 0 auto; } .monitor { color: #9fd0ff; flex: 0 0 auto; } .comment { position: relative; color: #d5d9df; flex: 1 1 auto; min-width: 0; overflow: hidden; white-space: nowrap; } .comment-track { display: inline-flex; align-items: center; white-space: nowrap; will-change: transform; } .comment.marquee .comment-track { animation: marquee linear infinite; animation-duration: var(--marquee-duration); } .comment-text { white-space: nowrap; flex: 0 0 auto; } .comment-text.clone { display: none; } .comment.marquee .comment-text.clone { display: inline-block; } .comment-gap { width: 80px; flex: 0 0 auto; } .link { color: #7fc1ff; flex: 0 0 auto; } .clock { color: #ffffff; flex: 0 0 auto; } .divider { color: rgba(255, 255, 255, 0.35); flex: 0 0 auto; font-weight: 400; transform: translateY(-1px); } @keyframes marquee { from { transform: translateX(0); } to { transform: translateX(calc(-1 * var(--marquee-shift))); } }
А вот весь js (скрипты):
const monitorType = document.body.dataset.monitor; const monitorText = monitorType === "left" ? "Источник: монитор слева" : monitorType === "webcam" ? "Источник: веб-камера" : "Источник: центральный монитор"; document.getElementById("monitor").textContent = monitorText; let lastCommentText = ""; async function updateComment() { try { const r = await fetch("stream_comment.txt?" + Date.now()); const text = (await r.text()).trim(); if (text === lastCommentText) { return; } lastCommentText = text; const comment = document.getElementById("comment"); const track = document.getElementById("comment-track"); const text1 = document.getElementById("comment-text-1"); const text2 = document.getElementById("comment-text-2"); const gap = comment.querySelector(".comment-gap"); comment.classList.remove("marquee"); track.style.animation = "none"; text1.textContent = text; text2.textContent = text; void track.offsetWidth; const containerWidth = comment.clientWidth; const textWidth = text1.scrollWidth; const gapWidth = gap.offsetWidth; const shift = textWidth + gapWidth; if (textWidth > containerWidth) { const speed = 80; const duration = shift / speed; comment.style.setProperty("--marquee-shift", shift + "px"); comment.style.setProperty("--marquee-duration", duration + "s"); track.style.animation = ""; comment.classList.add("marquee"); } else { comment.style.removeProperty("--marquee-shift"); comment.style.removeProperty("--marquee-duration"); track.style.animation = ""; comment.classList.remove("marquee"); } } catch {} } function updateClock() { const now = new Date(); const h = String(now.getHours()).padStart(2,'0'); const m = String(now.getMinutes()).padStart(2,'0'); const d = String(now.getDate()).padStart(2,'0'); const mo = String(now.getMonth()+1).padStart(2,'0'); const y = now.getFullYear(); document.getElementById("clock").textContent = `${h}:${m} ${d}.${mo}.${y}`; } setInterval(updateClock,1000); setInterval(updateComment,1000); updateClock(); updateComment();
Устанавливается это просто. Создаём папку, кидаем в неё все получившиеся файлы.

В самом OBS добавляем на сцену новый источник (выбираем тип «Browser»), указываем нужную html-ку, задаём высоту и ширину — и готово.


Наверняка можно было придумать решение и получше. Более того, я уверен, что через пару десятков стримов произойдёт несколько оптимизаций. Но для начала оно полностью решает мою задачу.
В итоге понедельничный стрим прошёл отлично. Я и в Axure поработал, и с ChatGPT пообщался, и в Codex повайбкодил, и в свою IDE потаскал результат, и потестировал, и… Да что там — обыкновенный рабочий процесс. Единственный момент: я кучу времени настраивал себе свет, а перед началом стрима забыл его включить.
А за первым стримом прошло ещё два. И я пока не собираюсь останавливаться (поставил себе цель на 100 стримов либо каждый день, либо кроме выходных, ещё не определился). Сегодня, например, буду писать функциональную спецификацию к НормЦРМ и объяснять, как это делается и, главное, зачем.
А про ChatGPT для себя уяснил одну вещь: есть области, в которых он разбирается хорошо и уверенно (например, разработка в стеке Питон + Джанго). А есть те, в которых общие знания у него есть, но здравого смысла, как эти знания применять эффективно и рационально — нет. Потому что, видимо, не было статей, которые разъясняют, как делать — правильно, а как — не очень.
Если бы я сразу сформулировал задачу как «сверстай HTML-оверлей из таких-то параметров» — сэкономил бы две трети времени.
