Русский
Русский
English
Статистика
Реклама

Rtmp

Стриминг множества RTSP IP камер на YouTube иили Facebook

07.06.2021 18:08:54 | Автор: admin

Как известно, у YouTube отсутствует фича захвата RTSP потока. Возможно, это сделано не случайно, а исходя из голой прагматики, чтобы люди не повесили на YouTube статическое видеонаблюдение за своими подъездами и не утилизировали его каналы, которые, как оказалось в пандемию, вовсе не резиновые. Напомним, что некоторое время назад имели место истории с ухудшением и ограничением качества стримов до 240p. Или есть еще одно предположение: стримы с IP камер это зло для YouTube, потому что у них чуть более ноля зрителей, на которых не накрутишь миллион просмотров рекламы. Так или иначе, фича не представлена, и мы постараемся заполнить этот пробел - помочь YouTube осчастливить пользователей.

Допустим, мы хотим взять обычную уличную IP камеру, которая отдает H.264 поток по RTSP и перенаправить ее на YouTube. Для этого потребуется принять RTSP поток и сконвертировать в RTMPS поток, который принимает YouTube. Почему именно в RTMPS, а не RTMP? Как известно несекьюрные протоколы отмирают. HTTP предан гонениям, и его участь постигла другие протоколы, не имеющие буквы S - значит Secure на конце. От RTMP потока отказался Facebook, но спасибо, оставил RTMPS. Итак, конвертируем RTSP в RTMPS. Делаем это Headless способом (без использования UI), т.е. на сервере.

Для одного RTSP потока потребуется один YouTube аккаунт, который будет принимать поток. Но что делать если камер не одна, а много?

Да, можно насоздавать вручную несколько YouTube-аккаунтов, например, чтобы покрыть видеонаблюдением приусадебный участок. Но это с огромной вероятностью нарушит условия пользовательского соглашения. А если камер не 10, а все 50? Создавать 50 аккаунтов? А дальше что? Смотреть это как? В этом случае на помощь может прийти микшер, который объединит камеры в один поток.

Посмотрим, как это работает на примере двух RTSP камер. Результирующий поток mixer1 = rtsp1 + rtsp2. Отправляем стрим mixer1 на YouTube. Все работает - обе камеры идут в одном потоке. Здесь стоит заметить, что микширование - достаточно ресурсоемкая по использованию CPU операция.

При этом, так как мы уже имеем RTSP поток на стороне сервера, мы можем перенаправить этот поток на другие RTMP endpoints, не неся при этом дополнительных расходов по CPU и памяти. Просто снимаем трафик с RTSP стрима и тиражируем на Facebook, Twitch, куда угодно без дополнительного RTSP захвата и депакетизации.

Меня, как девопса, будет мучать совесть, если я не заскриптую то, что можно было бы заскриптовать. Автоматизации способствует наличие REST API для управления захватом видео с камеры и ретрансляцией.

Например, с помощью запроса:

/rtsp/startup

можно захватить видеопоток от IP камеры.

Запрос:

/rtsp/find_all

позволит получить список захваченных сервером RTSP потоков.

Запрос для завершения RTSP сессии выглядит так:

/rtsp/terminate

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

Давайте подробно рассмотрим, как это можно сделать.

Небольшой мануал, как с помощью минимального кода организовать Live трансляцию на YouTube и Facebook

В качестве серверной части мы используем demo.flashphoner.com. Для быстрого развертывания своего WCS сервера воспользуйтесь этой инструкцией или запустите один из виртуальных инстансов на Amazon, DigitalOcean или в Docker.

Предполагается, что у вас имеется подтвержденный аккаунт на YouTube и вы уже создали трансляцию в YouTube Studio, а так же создали прямую видеотрансляцию в своем аккаунте на Facebook.

Для работы Live трансляций на YouTube и Facebook нужно указать в файле настроек WCS flashphoner.properties следующие строки:

rtmp_transponder_stream_name_prefix= Убирает все префиксы для ретранслируемого потока.

rtmp_transponder_full_url=true В значении "true" игнорирует параметр "streamName" и использует RTMP адрес для ретрансляции потока в том виде, в котором его указал пользователь.

rtmp_flash_ver_subscriber=LNX 76.219.189.0 - для согласования версий RTMP клиента между WCS и YouTube.

Теперь, когда все подготовительные действия выполнены, перейдем к программированию. Разместим в HTML файле минимально необходимые элементы:

Подключаем скрипты основного API и JS скрипт для работы live трансляции, который мы создадим чуть позже:

 <script type="text/javascript" src="../../../../flashphoner.js"></script> <script type="text/javascript" src="rtsp-to-rtmp-min.js"></script>

Инициализируем API на загрузку web-страницы:

<body onload="init_page()">

Добавляем нужные элементы и кнопки поля для ввода уникальных кодов потоков для YouTube и Facebook, кнопку для републикации RTSP потока, div элемент для вывода текущего статуса работы программы и кнопку для остановки републикации:

<input id="streamKeyYT" type="text" placeholder="YouTube Stream key"/><input id="streamKeyFB" type="text" placeholder="FaceBook Stream key"/><button id="repubBtn">Start republish</button><div id="republishStatus"></div><br><button id="stopBtn">Stop republish</button>

Затем переходим к созданию JS скрипта для работы републикации RTSP. Скрипт представляет собой мини REST клиент.

Создаем константы:

Константа "url", в которую записываем адрес для запросов REST API . Замените "demo.flashphoner.com" на адрес своего WCS.

Константа "rtspStream" указываем RTSP адрес потока с IP камеры. Мы для примера используем RTSP поток с виртуальной камеры.

var url = "https://demo.flashphoner.com:8444/rest-api";var rtspStream = "rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov"

Функция "init_page()" инициализирует основной API при загрузке web - страницы. Так же в этой функции прописываем соответствие кнопок вызываемым функциям и вызываем функцию "getStream", которая захватывает RTSP видеопоток с IP камеры:

function init_page() {Flashphoner.init({});repubBtn.onclick = streamToYouTube;stopBtn.onclick = stopStream;getStream();}

Функция "getStream()" отправляет на WCS REST запрос /rtsp/startup который захватывает видеопоток RTSP адрес которого был записан в константу rtspStream

function getStream() {    fetchUrl = url + "/rtsp/startup";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "uri": rtspStream        }),    }    fetch(fetchUrl, options);console.log("Stream Captured");}

Функция "streamToYouTube()" републикует захваченный видеопоток в Live трансляцию на YouTube:

function streamToYouTube() {fetchUrl = url + "/push/startup";const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "streamName": rtspStream,"rtmpUrl": "rtmp://a.rtmp.youtube.com/live2/"+document.getElementById("streamKeyYT").value        }),    }  fetch(fetchUrl, options);streamToFB()}

Эта функция отправляет на WCS REST вызов /push/startup в параметрах которого передаются следующие значения:

"streamName" - имя потока, который мы захватили с IP камеры. Имя потока соответствует его RTSP адресу, который мы записали в константу "rtspStream"

"rtmpUrl" - URL сервера + уникальный код потока. Эти данные выдаются при создании Live трансляции в YouTube Studio. В нашем примере мы жестко закрепили URL в коде, вы можете добавить для него еще одно поле на свою web страницу. Уникальный код потока указывается в поле "streamKeyYT" на нашей Web странице.

Функция "streamToFB" републикует захваченный видеопоток в Live трансляцию на Facebook:

function streamToFB() {fetchUrl = url + "/push/startup";const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "streamName": rtspStream,"rtmpUrl": "rtmps://live-api-s.facebook.com:443/rtmp/"+document.getElementById("streamKeyFB").value        }),    }  fetch(fetchUrl, options);document.getElementById("republishStatus").textContent = "Stream republished";}

Эта функция так же отправляет на WCS REST вызов "/push/startup" в параметрах которого передаются значения:

"streamName" - имя потока, который мы захватили с IP камеры. Имя потока соответствует его RTSP адресу, который мы записали в константу "rtspStream"

"rtmpUrl" - URL сервера + уникальный код потока. Эти данные можно найти на странице Live трансляции в Facebook в секции Live API. Url сервера в этой функции мы указали в коде, как и для функции републикации на YouTube. Уникальный код потока берем из поля "streamKeyFB" на Web странице.

Функция "stopStream()" отправляет RTSP запрос "/rtsp/terminate" который прекращает захват потока с IP камеры на WCS и соответственно прекращает публикации на YouTube и Facebook:

function stopStream() {fetchUrl = url + "/rtsp/terminate";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "uri": document.getElementById("rtspLink").value        }),    }    fetch(fetchUrl, options);document.getElementById("captureStatus").textContent = null;document.getElementById("republishStatus").textContent = null;document.getElementById("stopStatus").textContent = "Stream stopped";}

Полные коды HTML и JS файлов рассмотрим немного ниже.

Итак. Сохраняем файлы и пробуем запустить.

Последовательность действий для тестирования

Создаем Live трансляцию в YouTube Studio. Копируем уникальный код видеопотока:

Открываем созданную ранее HTML страницу. Указываем в первом поле уникальный код видеопотока, который мы скопировали на YouTube:

Создаем Live трансляцию в своем аккаунте на Facebook. Копируем уникальный код видеопотока.

Возвращаемся на нашу web страничку, вставляем скопированный код во второе поле и нажимаем кнопку "Start republish

Теперь проверяем работу нашей републикации. Снова переходим в YouTube Studio и на Facebook, ждем несколько секунд и получаем превью потока.

Для завершения републикации нажмите кнопку "Stop"

Теперь, как и обещали, исходные коды примера полностью:

Листинг HTML файла "rtsp-to-rtmp-min.html"

<!DOCTYPE html><html lang="en">    <head>        <script type="text/javascript" src="../../../../flashphoner.js"></script>        <script type="text/javascript" src="rtsp-to-rtmp-min.js"></script>    </head>    <body onload="init_page()">        <input id="streamKeyYT" type="text" placeholder="YouTube Stream key" /> <input id="streamKeyFB" type="text" placeholder="Facebook Stream key" /> <button id="repubBtn">Start republish</button>        <div id="republishStatus"></div>        <br />        <button id="stopBtn">Stop republish</button>    </body></html>

Листинг JS файла "rtsp-to-rtmp-min.js":

var url = "https://demo.flashphoner.com:8444/rest-api";var rtspStream = "rtsp://wowzaec2demo.streamlock.net/vod/mp4:BigBuckBunny_115k.mov"function init_page() {    Flashphoner.init({});    repubBtn.onclick = streamToYouTube;    stopBtn.onclick = stopStream;    getStream();}function getStream() {    fetchUrl = url + "/rtsp/startup";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "uri": rtspStream        }),    }    fetch(fetchUrl, options);    console.log("Stream Captured");}function streamToYouTube() {    fetchUrl = url + "/push/startup";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "streamName": rtspStream,            "rtmpUrl": "rtmp://a.rtmp.youtube.com/live2/" + document.getElementById("streamKeyYT").value        }),    }    fetch(fetchUrl, options);    streamToFB()}function streamToFB() {    fetchUrl = url + "/push/startup";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "streamName": rtspStream,            "rtmpUrl": "rtmps://live-api-s.facebook.com:443/rtmp/" + document.getElementById("streamKeyFB").value        }),    }    fetch(fetchUrl, options);    document.getElementById("republishStatus").textContent = "Stream republished";}function stopStream() {    fetchUrl = url + "/rtsp/terminate";    const options = {        method: "POST",        headers: {            "Content-Type": "application/json"        },        body: JSON.stringify({            "uri": rtspStream        }),    }    fetch(fetchUrl, options);    document.getElementById("republishStatus").textContent = "Stream stopped";}

Для минимальной реализации требуется совсем немного кода. Конечно для итогового внедрения функционала еще потребуется небольшая доработка напильником - добавить стили на web страницу и разные проверки на валидность данных в код JS скрипта. Но это работает.

Удачного стриминга!

Ссылки

Наш демо сервер

WCS на Amazon EC2 - Быстрое развертывание WCS на базе Amazon

WCS на DigitalOcean - Быстрое развертывание WCS на базе DigitalOcean

WCS в Docker - Запуск WCS как Docker контейнера

Трансляция WebRTC видеопотока с конвертацией в RTMP - Функции сервера по конвертации WebRTC аудио видео потока в RTMP

Трансляция потокового видео с профессионального устройства видеозахвата (Live Encoder) по протоколу RTMP - Функции сервера по конвертации видеопотоков от Live Encoder в RTMP

HTML5-трансляции с RTSP-IP камер - Функции сервера по воспроизведению RTSP видеопотоков

Подробнее..

Запускаем свой RTMP сервер для стриминга

15.01.2021 10:11:34 | Автор: admin


Иногда YouTube или Twitch не подходят как стриминговая платформа скажем, если вы пилите портал с вебинарами или контентом 18+, нарушаете авторские права или хотите максимально отгородить свою трансляцию от остального интернета. У них есть много альтернатив как в виде сервисов (те же минусы, недостаток контроля и непредсказуемая политика), так и в виде self-hosted решений. Проблема опенсорсных стриминговых проектов в том, что все они начинаются с крохотной связки из пары технологий, а затем отчаянно пытаются вырасти в сервис, добавляя сложные веб-интерфейсы, чаты, библиотеки стримов и в конечном счёте отдаляясь от исходной цели: дать миру инструмент, который по понятному мануалу позволит запустить свой сервер трансляций. Что с ним будет дальше, в какие системы будет встроена эта картинка это только ваше личное дело, а самописный аналог твича с лагающими и отваливающимися сервисами и периодически валящимся билдом не нужен никому, кроме его разработчиков. Поэтому в этой статье мы разберём минимальную цепочку действий для запуска своего RTMP-сервера с плеером.

Структура




Здесь всё просто: за приём и кодировку потока из OBS отвечает RTMP модуль Nginx'a. Сконвертированный поток он выставляет наружу, где его подбирает HLS (HTTP Live Streaming) клиент в браузере и выдаёт уже готовую картинку в плеере.

Установка


При выборе сервера упор стоит обратить внимание на процессор. Я взял эпичный сервер с двумя ядрами и пробовал наращивать битрейт, чтобы определить граничные условия на 11-12k нагрузка стала болтаться в районе 96-100%, так что для обработки действительно тяжёлого потока лучше взять мощности с запасом:



Нам понадобится Docker для установки контейнеризованного nginx-rtmp с FFmpeg и любой веб-сервер (включая тот же Nginx) для раздачи страницы с плеером. Я ставил на Ubuntu 20.04:

$ sudo apt-get update$ sudo apt-get install \  apt-transport-https \  ca-certificates \  curl \  gnupg-agent \  software-properties-common \  nginx$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -$ sudo apt-key fingerprint 0EBFCD88$ sudo add-apt-repository \  "deb [arch=amd64] https://download.docker.com/linux/ubuntu \  $(lsb_release -cs) \  stable"$ sudo apt-get update$ sudo apt-get install docker-ce docker-ce-cli containerd.io


Запускаем контейнер c проброшенными портами:

docker run -d -p 1935:1935 -p 8080:80 --rm alfg/nginx-rtmp


Затем в OBS на клиенте указываем наш сервер с произвольным ключом потока (ключ = индентификатор стрима):



Теперь можно запустить трансляцию и удостовериться что поток пошёл, например, в демке hls.js или в любом другом плеере HLS.

Осталось настроить сервер. В nginx.conf укажите путь до вашей страницы:

location / {                                                      root /var/www/;                                                    index index.htm index.html;                                   autoindex on;                                }


sudo nginx -s reload


В index.html просто скопипастим код из примера hls.js:

  <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/hls.js@latest"></script>  <!-- Or if you want a more recent alpha version -->  <!-- <script src="http://personeltest.ru/aways/cdn.jsdelivr.net/npm/hls.js@alpha"></script> -->  <video id="video"></video>  <script>    var video = document.getElementById('video');    var videoSrc = 'https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8';    if (Hls.isSupported()) {      var hls = new Hls();      hls.loadSource(videoSrc);      hls.attachMedia(video);      hls.on(Hls.Events.MANIFEST_PARSED, function() {        video.play();      });    }    // hls.js is not supported on platforms that do not have Media Source    // Extensions (MSE) enabled.    //    // When the browser has built-in HLS support (check using `canPlayType`),    // we can provide an HLS manifest (i.e. .m3u8 URL) directly to the video    // element through the `src` property. This is using the built-in support    // of the plain video element, without using hls.js.    //    // Note: it would be more normal to wait on the 'canplay' event below however    // on Safari (where you are most likely to find built-in HLS support) the    // video.src URL must be on the user-driven white-list before a 'canplay'    // event will be emitted; the last video event that can be reliably    // listened-for when the URL is not on the white-list is 'loadedmetadata'.    else if (video.canPlayType('application/vnd.apple.mpegurl')) {      video.src = videoSrc;      video.addEventListener('loadedmetadata', function() {        video.play();      });    }  </script>


Теперь на 8080 порту нашего сервера раздаётся жутковатый мультик про зайца:



Остаётся только изменить путь на http://server_ip:8080/live/stream-key.m3u8 и идти смотреть трансляцию!



Нагрузку в реальном времени можно проверять командой docker stats:



Заключение


Размещая стриминговый клиент на своём сервере важно помнить, что весь трафик со всех зрителей будет проходить прямо через него значит, если одновременный онлайн у вас будет больше 1-2 человек, стоит изучать способы распределения нагрузки (ведь транскодирвоание ощутимо давит и на CPU). Для запуска полноценного кластера есть энтерпрайзное (но опенсорсное) решение SRS aka Simple Realtime Server (GitHub, 10k звёзд, огромная вики, сложная архитектура). В него стоит вникать, если вам стримы нужны для решения настоящих задач, а не чтобы поиграться с приватным видеопотоком.



На правах рекламы


Серверы в аренду для любых задач это про наши эпичные! Все серверы защищены от DDoS-атак, автоматическая установка множества ОС или использование своего образа ISO. Лучше один раз попробовать!

Подробнее..

Как получить субтитрированный поток в RTMP из SDI

15.02.2021 12:06:03 | Автор: admin

Возникла задача получить из SDI сигнала трансляцию с субтитрами и отдать её на CDN в формате RTMP потока. Пару недель мучения и мытарств изложу в кратком содержании всех серий для коллекции. Возможно, кому пригодится.

Начнем с того какое оборудование было использовано для решения данной задачи:

Для захвата потока с SDI и транскодирования использовался сервер со следующей конфигурацией:

  1. Плата захвата, тестировалось две платы, Blackmagic DeckLink Duo 2 и DeckLink Quad 2, обе оправдали наши ожидания.

  2. Видеокарта с аппаратной поддержкой х264 кодека Nvidia Quadro P4000

  3. Сервер на базе процессора Intel(R) Xeon(R) Silver 4114

  4. Память 64Гб

Для отправки потока в сторону CDN использовался:

Wowza Streaming Engine сервер версии не ниже 8.5.

Сам захват с карты и передачу потока на Wowza было решено осуществлять средствами опенсорц проекта FFmpeg. Данный продукт хорошо себя зарекомендовал ранее и одно неоспоримое преимущество среди прочих он бесплатен.
Но для того чтобы все заработало нам необходимо собрать FFmpeg с необходимым перечнем модулей, а именно:

  • Поддержка DeckLink.

    Для этого необходимо скачать с сайта производителя Blackmagic_DeckLink_SDK желательно версии не ниже 10.7, на текущий момент присутствует версия 12. https://blackmagicdesign.com Blackmagic_DeckLink_SDK_12.0.zip
    Скачиваем распаковываем в дальнейшем нам потребуется указать путь к библиотекам при сборке нашего FFmpeg.

  • Поддержка аппаратного декодирования

    Необходимо зайти на сайт Nvidia и в разделе для разработчиков скачать CUDA под свою операционную систему.

    wget https://developer.download.nvidia.com/compute/cuda/11.2.0/local_installers/cuda_11.2.0_460.27.04_linux.runsudo sh cuda_11.2.0_460.27.04_linux.run
    
  • Декодирование Subtitles из SDI потока ZVBI

    В большинстве репозиториев уже присутствует данная библиотека в случае отсутствия её можно взять на https://sourceforge.net/projects/zapping/files/zvbi/0.2.35/

  • По необходимости другие библиотеки в частности, поддержка кодирования аудио в acc формат libfdk-aac и др.

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

Собрали до кучи все необходимые библиотеки и можем приступить к сборке самого FFmpeg со всеми необходимыми нам модулями.

В моем случае это примерно выглядело следующим образом:

--enable-cuda --enable-cuvid --enable-nvenc --enable-nonfree --enable-libnpp --extra-cflags=-I//cuda/include --extra-ldflags=-L//cuda/lib64 --enable-libfdk-aac --extra-cflags=-I//BlackmagicSDK/Linux/include --extra-ldflags=-L//BlackmagicSDK/Linux/include --enable-decklink --enable-libzvbi

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

После сборки тщательно обработать напильником, чем мы и займемся

Для захвата потока запустим FFmpeg со следующими ключами:

ffmpeg


-hwaccel cuvid использовать аппаратное кодирование (задействовать видеокарту)
-f decklink подключить драйвера для работы с картой захвата
-thread_queue_size 16384 задаем длину очереди потока по умолчанию 8 у Вас может отличаться
-teletext_lines all указываем где искать телетекст
-i DeckLink Quad (1) используем первый вход с платы захвата
-c:v h264_nvenc устанавливаем аппаратный кодек
-aspect 16:9 -s 1024x576 -filter:v yadif -profile:v main -level 3.1 -preset llhq -gpu any -rc cbrldhq
-g 50 -r 25 -minrate 2000k -b:v 2000k -maxrate 2000k -bufsize 4000k -pixfmt yuv420p
-c:a libfdk-aac -ar 44100 -ac 2 -ab 128k -af volume=10dB -loglevel warning

-metadata:s:s:0 language=rus если при захвате потока не идентифицируется язык то необходимо, обязательно, его прописать, в моем случае это был единственны субтитрированный поток и он не идентифицировался - имел язык und
-f mpegts udp://ХХХ.ХХХ.ХХ.12:6970?pkt_size=1316 таргетируем захваченный поток по направлению к серверу в формате mpegts (данный формат позволяет передавать несколько субпотоков и он поддерживается FFmpeg), так же возможно использование мультикаст адресов в случае необходимости одновременной обработки с одного потока.

В результате мы получим поток с тремя субпотоками (Video, Audio, Subtitles) что мы и добивались!

Теперь для захвата и отправки данного потока на стороне WOWZA сервера необходимо в нужном приложении создать Stream File со следующим содержимым:
по средствам веб или руками в [wowza]/content/

{uri: udp://XXX.XXX.XXX.12:6970?pkt_size=1316,  адрес потока mpegtsDVBTeletextType: 1,2,3,4,5,  типы субтитров, я указал все, но можно указать только 2 и 5 если не ошибаюсь.mpegtsDVBTeletextPageNumber: 88,  страница субтитров 888 (здесь нет опечатки)reconnectWaitTime: 3000,  время через которое необходимо осуществить переподключение в мсstreamTimeout: 5000  время ожидания при обрыве потока в мс}

Далее подключаемся к данному потоку и если все сделали правильно, то увидим его в Incoming Streams вашего приложения.

После чего можем перейти в Stream Targets вашего приложения и отправить поток по назначению, в формате RTMP в котором будет три субпотока (Video, Audio и Data).

На этом все пинайте, не сильно, за объективную критику, Спасибо!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru