ru:https://highload.today/blogs/kak-sozdat-potok-videodannyh-s-pomoshhyu-node-js-razbirayu-detali-na-svoem-proekte/ ua:https://highload.today/uk/blogs/yak-stvoryty-potik-videodanyh-za-dopomogoyu-node-js/
logo
Back-end      05/08/2022

Как создать поток видеоданных с помощью Node.js: разбираю детали на своем проекте

Владислав Хирса BLOG

Senior Backend Developer | Node.js | NestJS | Express.js | SQL | NoSQL | AWS

Привет всем! Меня зовут Владислав Хирса, я — Software Engineer в Grid Dynamics. Сегодня я расскажу вам, как с помощью Node.js создать поток видеоданных. Статья будет полезна тем, кто еще только начинает разбираться в теме Streams in Node.js (в конце будет немного важной теории).

Создать поток данных в Node.js сейчас просто, но все ли мы понимаем о том, как работает эта абстракция?

Найти код вы можете по ссылке.

Начнем!

Сначала запустим наш сервер, находясь в папке проекта, командой npm start.

Далее наш сервер запустится по адресу http://localhost:8000/. Перейдя по ссылке, в вашем браузере должна появиться вкладка следующего содержания:

Здесь мы можем увидеть в действии наш проект. И теперь то, ради чего мы здесь — узнать, как все работает.

Файл index.js

Нажмите для просмотра

Нажмите для просмотра

Здесь мы создали простой сервер, функцией обратного вызова назначили функцию router, получающую параметры request и response. Далее мы проверяем, имеем ли по полученному request.url совпадению в нашем объекте runnersByRouts по данному имени ключа. Если да — то вызываем соответствующую функцию, если нет — то возвращаем ответ об ошибке к клиенту.

При открытии вкладки в браузере по нашей ссылке на сервер поступает запрос с url / и мы отдаем нашу страницу, файл index.html.

Онлайн-курс Бізнес-аналіз. Basic Level від Ithillel.
В ході курсу студенти навчаться техніці збору і аналізу вимог, документуванню та управлінню документацією, управлінню ризиками та змінами, а також навчаться моделювати процеси і прототипуванню.
Приєднатися

Файл src/send-home-page.js

Нажмите для просмотра

Нажмите для просмотра

Сначала мы находим путь к нашей папке с помощью url.fileURLToPath(new URL('.', import.meta.url)), назначаем тип контента, который собираемся отправить клиенту res.setHeader('Content-Type', 'text/html'), далее создаем читаемый поток fs.createReadStream(__dirname + '../public/index.html')и на последней строке вызываем наш поток с помощью функции pipeline().

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

Файл index.html

Нажмите для просмотра

Нажмите для просмотра

Рассмотрим несколько важных для нас атрибутов в html-элементе <video>:

  1. src="/video-stream"— при рендеринге нашей страницы в браузере мы обращаемся к серверу по адресу http://localhost:8000/video-stream и получаем наше видео.
  2. controls— этот атрибут позволяет пользователю иметь контроль над видео (старт/пауза, звук и т.п.).
  3. preload="auto"— в спецификации указано, что весь видеофайл может загрузиться даже если пользователь не будет использовать его. Но на практике все зависит от браузера и будет происходить скорее всего более динамично. Например, да — ваше видео будет загружено примерно на 1 мин. наперед и через каждые 5 секунд воспроизведенного видео дозагрузятся еще 5 секунд и т.д.

Поэтому нам поступает запрос с url /video-stream и мы вызываем нашу функцию sendVideoFile.

Файл src/send-video-file.js

Нажмите для просмотра

Нажмите для просмотра

В нашей функции sendVideoFile все начинается с того, что:

  1. Мы создаем абсолютный путь к файлу — путь, который нам указали в параметре pathToVideo. Было public/nature.mp4 — стало /your_folder/your_folder/project_folder/public/nature.mp4.
  2. Основи Web дизайну від Ithillel.
    Цей онлайн-курс з основ веб-дизайну дозволить вам опанувати мистецтво створення ефективних та привабливих інтерфейсів для вебсайтів і застосунків. Ви оволодієте ключовими принципами UX/UI дизайну, створюватимете дизайн-макети та прототипи, розроблятимете адаптивні інтерфейси для різних пристроїв, готуючись до професійної кар'єри в галузі веб-дизайну.
    Дізнатися більше
  3. fs.statSync(resolvedPath).size — узнаем размер файла в байтах.
  4. req.headers.range — получаем параметр range (bytes=12582912-), то есть то, с какой позиции нужно скачивать видео в байтах.

В зависимости от браузера и проигрывателя параметр range может быть null или, например, bytes=123456-, поэтому у нас есть две различные функции для обработки этих на самом деле разных подходов.

Файл utils/create-video-stream.js

Нажмите для просмотра

Нажмите для просмотра

Здесь уже все просто — код схож с тем, который мы уже рассматривали в src/send-home-page.js. Единственная разница в том, что мы назначаем обязательные заголовки Content-Type и Content-Length для того, чтобы браузер понимал, какого типа мы посылаем ему информацию и какого размера. Это необходимо как для корректной работы проигрывателя, так и для дальнейшего взаимодействия проигрывателя с сервером во время последующих транзакций данных.

И внизу также один из самых частых случаев — когда параметр range существует.

Файл utils/create-video-stream-by-range.js

Нажмите для просмотра

Нажмите для просмотра

Здесь у нас есть функция getChunkData, которая принимает входящий параметр range и fileSize осуществляет следующие шаги:

  1. Берет пару значений из range = bytes=36634624- и получает массив parts = [ '36634624', '' ].
  2. Вычисляет значение start = 36896768, end = 86890916, chunkSize = 49994149.

Файл utils/get-chunk-data.js

Нажмите для просмотра

Нажмите для просмотра

Также createVideoStreamByRange мы указываем обязательным статус ответа 206, а также Content-Range — какую часть данных со всего видео мы отправляем, а также Accept-Ranges — в каком формате данные, которые мы отправляем.

Еще несколько дополнений, которые полезно знать:

Онлайн-курс "Computer Vision" від robot_dreams.
Застосовуйте Machine Learning / Deep Learning та вчіть нейронні мережі розпізнавати об’єкти на відео. Отримайте необхідні компетенції Computer Vision Engineer.
Дізнатись більше про курс
  1. pipeline почему лучше использовать pipeline(), а не очередь pipe() при работе с потоками. В функции pipeline() последний аргумент — функция обратного вызова. Мы использовали ее в вышеперечисленных примерах кода. Если возникнет ошибка в любом из переданных потоков, то мы ее можем обработать в одном месте. Также pipeline() самостоятельно закрывает все оконченные, но не закрытые запросы к серверу. Например, когда мы используем someReadStream(path).pipe(res), то после ошибки или окончания передачи данных запрос на сервер скорее всего не закроет, из-за чего возникают непонятные и очень веские ошибки и потеря оперативной памяти. Об этом вы можете почитать подробнее здесь.
  2. ES modules — чтобы использовать импорт функционала с помощью imports, в js-файлах нам нужно указать в package.json тип таким образом { "type": "module" }, но такие переменные, как __dirname и __filename не существуют в ES modules, а есть возможность CommonJS. С этим вы можете ознакомиться по ссылке из официальной документации. Так что найти пути мы можем следующим образом:

const __filename = url.fileURLToPath(import.meta.url);

const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

  1. highWaterMark — это значение размера внутреннего буфера, то есть количество данных в байтах, которые мы можем прочитать за один раз, то есть один chunk данных (по умолчанию он 64kB). Также значение highWaterMark мы можем изменить при создании потока fs.createReadStream(path, { highWaterMark: 2 }), теперь мы считываем наш файл по два символа за раз, а также можем узнать его размер следующим образом: readStream.readableHighWaterMark, значение по умолчанию будет 65536 байтов.
Нажмите для просмотра

Нажмите для просмотра

Как работает поток и отправка данных в деталях?

Сначала мы создаем поток по считыванию файла и назначаем его в смену readStream, после этого используем его в функции pipeline(), далее chunk данных передается к потоку res (т.е. response, если полностью) и тогда res его получает и отправляет клиенту с помощью res.write(chunk). Каждый раз когда мы читаем и передаем ему наши chunk данные, то в конце, когда уже нет данных для считывания, вызывается событие end для каждого потока и функция pipeline()самостоятельно закрывает их. Что очень важно, в случае res после последнего вызывается res.end() и наш запрос к серверу успешно заканчивается.

На этом все, спасибо всем за внимание. Продуктивного вам кодинга 😉

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

Онлайн-курс "QA Automation" від robot_dreams.
Це 70% практики, 30% теорії та проєкт у портфоліо.Навчіться запускати перевірку сотень опцій одночасно, натиснувши лише одну кнопку.
Детальніше про курс

Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.

Топ-5 самых популярных блогеров марта

PHP Developer в ScrumLaunch
Всего просмотровВсего просмотров
2434
#1
Всего просмотровВсего просмотров
2434
Founder at Shallwe, Python Software Engineer (Django/React)
Всего просмотровВсего просмотров
113
#2
Всего просмотровВсего просмотров
113
Career Consultant в GoIT
Всего просмотровВсего просмотров
95
#3
Всего просмотровВсего просмотров
95
CEO & Founder в Trustee
Всего просмотровВсего просмотров
94
#4
Всего просмотровВсего просмотров
94
Рейтинг блогеров

Ваша жалоба отправлена модератору

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: