Решил с понедельника открыть сезон стримов на Ютубе. Идея банальная: показывать вживую, как я проектирую и вайбкодю пет-проект. Ну как пет-проект… В мае ему уже исполнится год и по архитектуре и функциям он разросся настолько, что уже приходится относиться к нему со всем уважением :-)

Пошёл в ChatGPT, поделился идеей. «Идея замечательная!» — сказал чат и начал уже было расхваливать меня, но я его остановил. «Мне нужна помощь с OBS: хочу сделать в стриме плашку с информацией: кто я, что прямо сейчас делаю, ссылки, время, вот это всё». «Спокойствие, сейчас всё объясню!» — сказал ChatGPT — и именно с этого началась моя история.

OBS — это программа, популярная среди стримеров. Она позволяет захватывать видео с экрана, вебку, управлять всем этим делом с помощью сцен и транслировать результат, куда прикажете. И, главное, она бесплатная.

Я описал ЧатуГПТ свой сетап:

  • Три монитора и вебкамера;

  • На левом и центральном мониторе я буду работать и стримить;

  • На правом у меня будет висеть OBS и всё, что я не хочу показывать (Телеграм, файлы с заметками, дополнительное окно браузера и т.п.).

И объяснил, какие мне нужны сцены:

  • Левый монитор плюс моя вебка в правом нижнем углу;

  • Центральный монитор плюс моя вебка в правом нижнем углу;

  • Вебка на весь экран — когда я хочу отвлечься от работы и поделиться какой-нибудь офигительной историей.

Делюсь офигительной историей о том, как пытался делать контент-маркетинг для ОБИТа
Делюсь офигительной историей о том, как пытался делать контент-маркетинг для ОБИТа

«И вот для каждой из этих сцен мне нужна сверху плашка, на которой будут…»

  • Имя стримера (моё имя, Егор Камелев);

  • Информация о том, какой сейчас транслируется монитор (или вебка);

  • Редактируемая текстовая информация о том, что именно происходит на стриме прямо сейчас;

  • Ссылки на продвигаемые ресурсы;

  • Дата и время (чтобы можно было понять — это живой стрим или запись).

Кстати, изначально ЧатГПТ сам мне предложил содержимое плашки, но оно не было похоже на то, которое я только что описал выше. Он советовал показывать статус стрима «Live» и его название. Мне эта идея не приглянулась. На мой вопрос «Зачем показывать статус стрима, если его потом всё равно многие будут смотреть в записи?» он не нашёлся что ответить.

В общем, с горем пополам определились с содержимым плашки — и чат начал выдавать мне инструкцию.

  • Сначала сделай чёрную полупрозрачную подложку (а должен заметить, что я пользуюсь Виндой и решил спрятать нижний бар с пуском, поиском и прочими иконками; высота бара составляет 48 пикселей, и именно такую высоту я и задал для плашки; естественно, полупрозрачность там была абсолютно не нужна, т.к. я обрезал транслируемый экран на эту высоту);

  • Теперь добавь текстовый блок с твоим именем. Настрой его. Молодец;

  • Теперь добавь текстовый блок с указанием источника стрима.

В этот момент я прерываю чат. Я бы хотел сделать вертикальные разделители между элементами. Куда мне их лучше засунуть? В качестве отдельных элементов или как часть каждого следующего элемента? Чат посоветовал второе.

  • Теперь давай добавим текст про тему стрима. В качестве источника возьмём текстовый файл на твоём компе. Ты сможешь редактировать его и как только сохранишь — текст обновится.

«О, это круто», — подумал я. Но тут увидел, что вертикальные разделители как-то странно висят в воздухе относительно текстов и мне это не понравилось. Поэтому пришлось для каждого разделителя добавлять отдельный элемент сцены.

  • Теперь добавим блок со ссылками. Молодец

  • А теперь часики. Для того, чтобы отображать реальное время мы можем пойти двумя путями:

    • Первый: используем текстовый файл, в котором ты будешь периодически менять время вручную (в этом месте инструкции нужно было видеть моё лицо);

    • Второй: используем такой источник данных как «Браузер» и сделаем html-страничку и небольшой js, который будет автоматически обновлять время.

И я такой в этом моменте: «Чегоооо?».

После этого я спросил: «Я правильно понимаю, что мы можем сверстать любой html, использовать свой css и js — и транслировать это в стрим, даже не открывая браузера?». «Да» — ответил ChatGPT.

И в этот момент я понял, что вся моя предыдущая работа с кучей элементов со своими настройками — полностью перечёркивается.

— А почему же ты мне тогда не предложил сразу сверстать плашку целиком?

— Я не знал, сможешь ли ты разобраться с вёрсткой — и предложил самый простой и быстрый вариант для новичка.

— А зачем мне разбираться с вёрсткой, если ты сам отлично всё можешь верстать, и от тебя потребовалась бы лишь инструкция о том, как мне прикрутить это дело к OBS?

— Да, конечно, ты совершенно прав. Так было бы и правильнее, и быстрее!

Пожалуй, это был один из немногих моментов, когда я испытал какие-то эмоции в адрес чатаГПТ.

Через пятнадцать минут всё было готово. Я с помощью чата сверстал три html-ки (для двух мониторов и вебки), связал их с текстовым файлом, в котором мог редактировать тему стрима на лету. Настройки стилей вынес в отдельный файлик. И также создал отдельный файлик для всех скриптов. Проблема висящих разделителей исчезла (как и любая другая проблема, связанная с оформлением). Десять элементов в источниках сцены превратились в один.

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

Поделюсь с вами кодом.

Вот overlay для центрального монитора (оверлеи для других сцен — по аналогии):

Это центральный монитор. Тут я показывал, как переношу ручками код из Кодекса в свою IDE
Это центральный монитор. Тут я показывал, как переношу ручками код из Кодекса в свою IDE
<!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-оверлей из таких-то параметров» — сэкономил бы две трети времени.

Ссылки на мои Хабростатьи по теме: